benjosaur commited on
Commit
b8a2da5
·
1 Parent(s): 293f790

Add MCP Server

Browse files
Files changed (9) hide show
  1. .env.example +2 -0
  2. .gitignore +22 -0
  3. DEPLOYMENT.md +144 -0
  4. README.md +99 -0
  5. app.py +251 -0
  6. pyproject.toml +15 -0
  7. requirements.txt +4 -0
  8. server.py +271 -0
  9. test_local.py +98 -0
.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ UPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
2
+ UPSTASH_REDIS_REST_TOKEN=your-token-here
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment
2
+ .env
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+ *.so
7
+ .Python
8
+
9
+ # Virtual environments
10
+ venv/
11
+ ENV/
12
+ env/
13
+
14
+ # IDEs
15
+ .vscode/
16
+ .idea/
17
+ *.swp
18
+ *.swo
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
DEPLOYMENT.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Guide
2
+
3
+ ## Deploying to Hugging Face Spaces
4
+
5
+ ### Step 1: Create a New Space
6
+
7
+ 1. Go to [Hugging Face Spaces](https://huggingface.co/spaces)
8
+ 2. Click "Create new Space"
9
+ 3. Choose:
10
+ - **SDK:** Gradio
11
+ - **Space name:** `ev-utility-function` (or your preferred name)
12
+ - **License:** Choose appropriate license
13
+ - **Visibility:** Public or Private
14
+
15
+ ### Step 2: Push Your Code
16
+
17
+ Option A: Using Git
18
+
19
+ ```bash
20
+ # Clone your space repository
21
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/ev-utility-function
22
+ cd ev-utility-function
23
+
24
+ # Copy all files from utility-mcp-server directory
25
+ cp -r /path/to/utility-mcp-server/* .
26
+
27
+ # Remove server.py if you only want the Gradio interface
28
+ # (Keep it if you want to document the MCP server too)
29
+
30
+ # Add and commit
31
+ git add .
32
+ git commit -m "Initial commit: EV Utility Function Calculator"
33
+ git push
34
+ ```
35
+
36
+ Option B: Using Hugging Face Web Interface
37
+
38
+ 1. Upload the following files through the web interface:
39
+ - `app.py`
40
+ - `requirements.txt`
41
+ - `README.md`
42
+ - `.gitignore`
43
+
44
+ ### Step 3: Configure Environment Variables
45
+
46
+ 1. Go to your Space settings
47
+ 2. Click on "Settings" → "Variables and secrets"
48
+ 3. Add the following secrets:
49
+ - `UPSTASH_REDIS_REST_URL`: Your Upstash Redis URL
50
+ - `UPSTASH_REDIS_REST_TOKEN`: Your Upstash Redis token
51
+
52
+ ### Step 4: Wait for Build
53
+
54
+ Your Space will automatically build and deploy. You can monitor the build logs in the "Logs" section.
55
+
56
+ ## Using as an MCP Server
57
+
58
+ ### With Claude Desktop
59
+
60
+ 1. Copy `server.py` to your local machine
61
+ 2. Create a `.env` file with your Upstash credentials
62
+ 3. Add to `claude_desktop_config.json`:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "ev-utility": {
68
+ "command": "python",
69
+ "args": ["/absolute/path/to/utility-mcp-server/server.py"],
70
+ "env": {
71
+ "UPSTASH_REDIS_REST_URL": "https://your-redis-url.upstash.io",
72
+ "UPSTASH_REDIS_REST_TOKEN": "your-token-here"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ 4. Restart Claude Desktop
80
+
81
+ ### With Other MCP Clients
82
+
83
+ The server uses stdio for communication. Any MCP-compatible client can connect to it.
84
+
85
+ ## Testing Locally
86
+
87
+ ### Test the Gradio App
88
+
89
+ ```bash
90
+ # Install dependencies
91
+ pip install -r requirements.txt
92
+
93
+ # Create .env file with your credentials
94
+ cp .env.example .env
95
+ # Edit .env with your actual credentials
96
+
97
+ # Run the app
98
+ python app.py
99
+ ```
100
+
101
+ ### Test the MCP Server
102
+
103
+ ```bash
104
+ # Run the MCP server
105
+ python server.py
106
+
107
+ # The server will communicate via stdin/stdout
108
+ # Use an MCP client to test, or integrate with Claude Desktop
109
+ ```
110
+
111
+ ## Troubleshooting
112
+
113
+ ### "No module named 'mcp'"
114
+
115
+ Install the MCP package:
116
+ ```bash
117
+ pip install mcp
118
+ ```
119
+
120
+ ### "Connection to Redis failed"
121
+
122
+ 1. Check your environment variables are set correctly
123
+ 2. Verify your Upstash Redis URL and token
124
+ 3. Ensure your IP is not blocked by Upstash
125
+
126
+ ### Space Build Fails
127
+
128
+ 1. Check the build logs in Hugging Face Spaces
129
+ 2. Ensure all dependencies are in `requirements.txt`
130
+ 3. Verify Python version compatibility (use Python 3.10+)
131
+
132
+ ## Updating the Space
133
+
134
+ To update your deployed Space:
135
+
136
+ ```bash
137
+ # Make your changes locally
138
+ # Commit and push
139
+ git add .
140
+ git commit -m "Update: description of changes"
141
+ git push
142
+ ```
143
+
144
+ The Space will automatically rebuild with your changes.
README.md CHANGED
@@ -11,3 +11,102 @@ license: mit
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
14
+
15
+ # EV Utility Function MCP Server
16
+
17
+ An MCP (Model Context Protocol) server that provides utility function calculations for electric vehicles based on user preferences.
18
+
19
+ ## Features
20
+
21
+ This MCP server exposes two tools:
22
+
23
+ ### 1. `calculate_utility`
24
+
25
+ Calculate the utility score for a single car based on a user's trained preferences.
26
+
27
+ **Parameters:**
28
+
29
+ - `user_id`: Username whose utility function to use
30
+ - `price`: Car price in euros
31
+ - `range`: Range in kilometers
32
+ - `efficiency`: Efficiency in Wh/km
33
+ - `acceleration`: 0-100km/h time in seconds
34
+ - `fast_charge`: Fast charging power in kW
35
+ - `seat_count`: Number of seats
36
+
37
+ **Returns:** JSON with utility score and coefficients used
38
+
39
+ ### 2. `find_best_car`
40
+
41
+ Find the best car from an array based on a user's utility function.
42
+
43
+ **Parameters:**
44
+
45
+ - `user_id`: Username whose utility function to use
46
+ - `cars`: Array of car objects with the above features
47
+
48
+ **Returns:** JSON with the best car and all cars ranked by utility
49
+
50
+ ## Setup
51
+
52
+ ### Local Development
53
+
54
+ 1. Install dependencies:
55
+
56
+ ```bash
57
+ pip install -e .
58
+ ```
59
+
60
+ 2. Create a `.env` file with your Upstash Redis credentials:
61
+
62
+ ```bash
63
+ UPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
64
+ UPSTASH_REDIS_REST_TOKEN=your-token-here
65
+ ```
66
+
67
+ 3. Run the MCP server:
68
+
69
+ ```bash
70
+ python server.py
71
+ ```
72
+
73
+ ### Using with Claude Desktop
74
+
75
+ Add to your `claude_desktop_config.json`:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "ev-utility": {
81
+ "command": "python",
82
+ "args": ["/path/to/utility-mcp-server/server.py"],
83
+ "env": {
84
+ "UPSTASH_REDIS_REST_URL": "https://your-redis-url.upstash.io",
85
+ "UPSTASH_REDIS_REST_TOKEN": "your-token-here"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## Hugging Face Space
93
+
94
+ This server is also available as a Hugging Face Space for easy web-based access and demonstration.
95
+
96
+ ## How It Works
97
+
98
+ 1. User preferences (coefficients) are stored in Upstash Redis with key format `params:{user_id}`
99
+ 2. The server fetches coefficients from Redis when calculating utilities
100
+ 3. Features are scaled consistently with the training data
101
+ 4. Utility is calculated as a dot product: `utility = Σ(coefficient_i × scaled_feature_i)`
102
+
103
+ ## Default Coefficients
104
+
105
+ If a user_id is not found in Redis, default coefficients are used:
106
+
107
+ - price: -0.5
108
+ - range: 0.8
109
+ - efficiency: -0.3
110
+ - acceleration: -0.5
111
+ - fast_charge: 0.6
112
+ - seat_count: 0.4
app.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio App for EV Utility Function Calculator
3
+ Deployed on Hugging Face Spaces
4
+ """
5
+
6
+ import gradio as gr
7
+ import json
8
+ import os
9
+ from upstash_redis import Redis
10
+
11
+ # Initialize Redis client
12
+ redis = Redis(
13
+ url=os.getenv("UPSTASH_REDIS_REST_URL"),
14
+ token=os.getenv("UPSTASH_REDIS_REST_TOKEN")
15
+ )
16
+
17
+ # Feature keys in order
18
+ FEATURE_KEYS = ["price", "range", "efficiency", "acceleration", "fast_charge", "seat_count"]
19
+
20
+ # Default coefficients
21
+ DEFAULT_COEFFS = {
22
+ "price": -0.5,
23
+ "range": 0.8,
24
+ "efficiency": -0.3,
25
+ "acceleration": -0.5,
26
+ "fast_charge": 0.6,
27
+ "seat_count": 0.4,
28
+ }
29
+
30
+
31
+ def get_user_coefficients(user_id: str) -> dict[str, float]:
32
+ """Fetch user coefficients from Redis."""
33
+ redis_key = f"params:{user_id}"
34
+ data_str = redis.get(redis_key)
35
+
36
+ if data_str is None:
37
+ return DEFAULT_COEFFS
38
+
39
+ if isinstance(data_str, bytes):
40
+ data_str = data_str.decode('utf-8')
41
+
42
+ data = json.loads(data_str) if isinstance(data_str, str) else data_str
43
+ return data.get("coeffs", DEFAULT_COEFFS)
44
+
45
+
46
+ def scale_features(car_features: dict[str, float]) -> list[float]:
47
+ """Scale car features."""
48
+ def get_val(key: str) -> float:
49
+ val = car_features.get(key)
50
+ if val is None:
51
+ return 0.0
52
+ return float(val)
53
+
54
+ scaled = [
55
+ get_val("price") / 1000,
56
+ get_val("range") / 100,
57
+ get_val("efficiency") / 10,
58
+ get_val("acceleration"),
59
+ get_val("fast_charge") / 100,
60
+ get_val("seat_count"),
61
+ ]
62
+ return scaled
63
+
64
+
65
+ def calculate_utility_score(car_features: dict[str, float], coeffs: dict[str, float]) -> float:
66
+ """Calculate utility score."""
67
+ scaled_features = scale_features(car_features)
68
+ coeff_array = [coeffs[key] for key in FEATURE_KEYS]
69
+ utility = sum(f * c for f, c in zip(scaled_features, coeff_array))
70
+ return utility
71
+
72
+
73
+ def calculate_single_utility(user_id: str, price: float, range_km: float, efficiency: float,
74
+ acceleration: float, fast_charge: float, seat_count: int) -> str:
75
+ """Calculate utility for a single car."""
76
+ try:
77
+ coeffs = get_user_coefficients(user_id)
78
+
79
+ car_features = {
80
+ "price": price,
81
+ "range": range_km,
82
+ "efficiency": efficiency,
83
+ "acceleration": acceleration,
84
+ "fast_charge": fast_charge,
85
+ "seat_count": seat_count
86
+ }
87
+
88
+ utility = calculate_utility_score(car_features, coeffs)
89
+
90
+ result = {
91
+ "user_id": user_id,
92
+ "utility_score": round(utility, 4),
93
+ "coefficients_used": {k: round(v, 4) for k, v in coeffs.items()},
94
+ "car_features": car_features,
95
+ "note": "Using default coefficients" if coeffs == DEFAULT_COEFFS else "Using saved user preferences"
96
+ }
97
+
98
+ return json.dumps(result, indent=2)
99
+ except Exception as e:
100
+ return json.dumps({"error": str(e)}, indent=2)
101
+
102
+
103
+ def find_best_from_list(user_id: str, cars_json: str) -> str:
104
+ """Find the best car from a JSON list."""
105
+ try:
106
+ cars = json.loads(cars_json)
107
+ coeffs = get_user_coefficients(user_id)
108
+
109
+ best_car = None
110
+ best_utility = float('-inf')
111
+ all_results = []
112
+
113
+ for car in cars:
114
+ utility = calculate_utility_score(car, coeffs)
115
+ car_result = {**car, "utility": round(utility, 4)}
116
+ all_results.append(car_result)
117
+
118
+ if utility > best_utility:
119
+ best_utility = utility
120
+ best_car = car_result
121
+
122
+ # Sort by utility descending
123
+ all_results.sort(key=lambda x: x["utility"], reverse=True)
124
+
125
+ result = {
126
+ "user_id": user_id,
127
+ "best_car": best_car,
128
+ "all_cars_ranked": all_results,
129
+ "coefficients_used": {k: round(v, 4) for k, v in coeffs.items()},
130
+ "note": "Using default coefficients" if coeffs == DEFAULT_COEFFS else "Using saved user preferences"
131
+ }
132
+
133
+ return json.dumps(result, indent=2)
134
+ except json.JSONDecodeError:
135
+ return json.dumps({"error": "Invalid JSON format"}, indent=2)
136
+ except Exception as e:
137
+ return json.dumps({"error": str(e)}, indent=2)
138
+
139
+
140
+ # Example cars JSON
141
+ example_cars = json.dumps([
142
+ {
143
+ "name": "Tesla Model 3",
144
+ "price": 45000,
145
+ "range": 500,
146
+ "efficiency": 150,
147
+ "acceleration": 6.1,
148
+ "fast_charge": 170,
149
+ "seat_count": 5
150
+ },
151
+ {
152
+ "name": "Volkswagen ID.4",
153
+ "price": 40000,
154
+ "range": 420,
155
+ "efficiency": 180,
156
+ "acceleration": 8.5,
157
+ "fast_charge": 125,
158
+ "seat_count": 5
159
+ },
160
+ {
161
+ "name": "Hyundai Ioniq 5",
162
+ "price": 48000,
163
+ "range": 480,
164
+ "efficiency": 165,
165
+ "acceleration": 7.4,
166
+ "fast_charge": 220,
167
+ "seat_count": 5
168
+ }
169
+ ], indent=2)
170
+
171
+
172
+ # Create Gradio interface
173
+ with gr.Blocks(title="EV Utility Function Calculator") as demo:
174
+ gr.Markdown("""
175
+ # 🚗 EV Utility Function Calculator
176
+
177
+ This tool calculates utility scores for electric vehicles based on user preferences.
178
+ User preferences are stored in Upstash Redis with their trained coefficients.
179
+
180
+ **Note:** If a user_id is not found, default coefficients will be used.
181
+ """)
182
+
183
+ with gr.Tab("Calculate Single Car Utility"):
184
+ gr.Markdown("### Calculate the utility score for a single car")
185
+
186
+ with gr.Row():
187
+ with gr.Column():
188
+ user_id_single = gr.Textbox(label="User ID", value="benjo", placeholder="Enter username")
189
+ price = gr.Number(label="Price (€)", value=45000)
190
+ range_km = gr.Number(label="Range (km)", value=500)
191
+ efficiency = gr.Number(label="Efficiency (Wh/km)", value=150)
192
+ acceleration = gr.Number(label="0-100km/h (seconds)", value=6.1)
193
+ fast_charge = gr.Number(label="Fast Charge (kW)", value=170)
194
+ seat_count = gr.Number(label="Seat Count", value=5, precision=0)
195
+
196
+ with gr.Column():
197
+ output_single = gr.Code(label="Result", language="json")
198
+
199
+ calc_btn = gr.Button("Calculate Utility", variant="primary")
200
+ calc_btn.click(
201
+ calculate_single_utility,
202
+ inputs=[user_id_single, price, range_km, efficiency, acceleration, fast_charge, seat_count],
203
+ outputs=output_single
204
+ )
205
+
206
+ with gr.Tab("Find Best Car"):
207
+ gr.Markdown("### Find the best car from a list based on user preferences")
208
+
209
+ with gr.Row():
210
+ with gr.Column():
211
+ user_id_best = gr.Textbox(label="User ID", value="benjo", placeholder="Enter username")
212
+ cars_json = gr.Code(
213
+ label="Cars (JSON Array)",
214
+ value=example_cars,
215
+ language="json",
216
+ lines=20
217
+ )
218
+
219
+ with gr.Column():
220
+ output_best = gr.Code(label="Result", language="json", lines=25)
221
+
222
+ find_btn = gr.Button("Find Best Car", variant="primary")
223
+ find_btn.click(
224
+ find_best_from_list,
225
+ inputs=[user_id_best, cars_json],
226
+ outputs=output_best
227
+ )
228
+
229
+ gr.Markdown("""
230
+ ---
231
+ ## About
232
+
233
+ This is the web interface for the EV Utility Function MCP Server.
234
+
235
+ ### MCP Server
236
+
237
+ This tool is also available as an MCP (Model Context Protocol) server that can be used with Claude Desktop
238
+ and other MCP-compatible clients.
239
+
240
+ **Repository:** [GitHub Link]
241
+
242
+ ### How it works:
243
+
244
+ 1. Users train their preferences through the AutoFinder app
245
+ 2. Preferences are saved to Upstash Redis as coefficients
246
+ 3. This tool fetches those coefficients and calculates utility scores
247
+ 4. Utility = Σ(coefficient × scaled_feature)
248
+ """)
249
+
250
+ if __name__ == "__main__":
251
+ demo.launch()
pyproject.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "utility-mcp-server"
3
+ version = "0.1.0"
4
+ description = "MCP server for EV utility function calculations"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "mcp>=1.0.0",
9
+ "upstash-redis>=1.1.1",
10
+ "python-dotenv>=1.0.0",
11
+ ]
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=5.0.0
2
+ upstash-redis>=1.1.1
3
+ python-dotenv>=1.0.0
4
+ mcp>=1.0.0
server.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server for EV Utility Function Calculations
4
+
5
+ This server provides tools to calculate utility scores for electric vehicles
6
+ using personalized coefficients stored in Upstash Redis.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from typing import Any
12
+ from dotenv import load_dotenv
13
+ from upstash_redis import Redis
14
+
15
+ from mcp.server.models import InitializationOptions
16
+ from mcp.server import NotificationOptions, Server
17
+ from mcp.server.stdio import stdio_server
18
+ from mcp.types import Tool, TextContent
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+
23
+ # Initialize Redis client
24
+ redis = Redis(
25
+ url=os.getenv("UPSTASH_REDIS_REST_URL"),
26
+ token=os.getenv("UPSTASH_REDIS_REST_TOKEN")
27
+ )
28
+
29
+ # Feature keys in order (must match the order in the coefficient array)
30
+ FEATURE_KEYS = ["price", "range", "efficiency", "acceleration", "fast_charge", "seat_count"]
31
+
32
+ # Default coefficients if user not found
33
+ DEFAULT_COEFFS = {
34
+ "price": -0.5,
35
+ "range": 0.8,
36
+ "efficiency": -0.3,
37
+ "acceleration": -0.5,
38
+ "fast_charge": 0.6,
39
+ "seat_count": 0.4,
40
+ }
41
+
42
+
43
+ def get_user_coefficients(user_id: str) -> dict[str, float]:
44
+ """
45
+ Fetch user coefficients from Redis. Returns default if not found.
46
+ """
47
+ redis_key = f"params:{user_id}"
48
+ data_str = redis.get(redis_key)
49
+
50
+ if data_str is None:
51
+ return DEFAULT_COEFFS
52
+
53
+ # Parse the JSON data - handle both string and bytes
54
+ if isinstance(data_str, bytes):
55
+ data_str = data_str.decode('utf-8')
56
+
57
+ data = json.loads(data_str) if isinstance(data_str, str) else data_str
58
+ return data.get("coeffs", DEFAULT_COEFFS)
59
+
60
+
61
+ def scale_features(car_features: dict[str, float]) -> list[float]:
62
+ """
63
+ Scale car features to match the training data scaling.
64
+ """
65
+ def get_val(key: str) -> float:
66
+ val = car_features.get(key)
67
+ if val is None:
68
+ return 0.0
69
+ return float(val)
70
+
71
+ # ORDER MATTERS: Must match FEATURE_KEYS
72
+ scaled = [
73
+ get_val("price") / 1000,
74
+ get_val("range") / 100,
75
+ get_val("efficiency") / 10,
76
+ get_val("acceleration"),
77
+ get_val("fast_charge") / 100,
78
+ get_val("seat_count"),
79
+ ]
80
+ return scaled
81
+
82
+
83
+ def calculate_utility_score(car_features: dict[str, float], coeffs: dict[str, float]) -> float:
84
+ """
85
+ Calculate utility score using dot product of features and coefficients.
86
+ """
87
+ scaled_features = scale_features(car_features)
88
+ coeff_array = [coeffs[key] for key in FEATURE_KEYS]
89
+
90
+ # Dot product
91
+ utility = sum(f * c for f, c in zip(scaled_features, coeff_array))
92
+ return utility
93
+
94
+
95
+ # Create the MCP server
96
+ server = Server("utility-function")
97
+
98
+
99
+ @server.list_tools()
100
+ async def handle_list_tools() -> list[Tool]:
101
+ """
102
+ List available tools.
103
+ """
104
+ return [
105
+ Tool(
106
+ name="calculate_utility",
107
+ description="Calculate the utility score for a car based on a user's preferences",
108
+ inputSchema={
109
+ "type": "object",
110
+ "properties": {
111
+ "user_id": {
112
+ "type": "string",
113
+ "description": "The username/ID whose utility function to use"
114
+ },
115
+ "price": {
116
+ "type": "number",
117
+ "description": "Car price in euros"
118
+ },
119
+ "range": {
120
+ "type": "number",
121
+ "description": "Range in kilometers"
122
+ },
123
+ "efficiency": {
124
+ "type": "number",
125
+ "description": "Efficiency in Wh/km"
126
+ },
127
+ "acceleration": {
128
+ "type": "number",
129
+ "description": "0-100km/h time in seconds"
130
+ },
131
+ "fast_charge": {
132
+ "type": "number",
133
+ "description": "Fast charging power in kW"
134
+ },
135
+ "seat_count": {
136
+ "type": "number",
137
+ "description": "Number of seats"
138
+ }
139
+ },
140
+ "required": ["user_id", "price", "range", "efficiency", "acceleration", "fast_charge", "seat_count"]
141
+ }
142
+ ),
143
+ Tool(
144
+ name="find_best_car",
145
+ description="Find the best car from an array based on a user's utility function",
146
+ inputSchema={
147
+ "type": "object",
148
+ "properties": {
149
+ "user_id": {
150
+ "type": "string",
151
+ "description": "The username/ID whose utility function to use"
152
+ },
153
+ "cars": {
154
+ "type": "array",
155
+ "description": "Array of car objects with features",
156
+ "items": {
157
+ "type": "object",
158
+ "properties": {
159
+ "name": {"type": "string"},
160
+ "price": {"type": "number"},
161
+ "range": {"type": "number"},
162
+ "efficiency": {"type": "number"},
163
+ "acceleration": {"type": "number"},
164
+ "fast_charge": {"type": "number"},
165
+ "seat_count": {"type": "number"}
166
+ },
167
+ "required": ["price", "range", "efficiency", "acceleration", "fast_charge", "seat_count"]
168
+ }
169
+ }
170
+ },
171
+ "required": ["user_id", "cars"]
172
+ }
173
+ )
174
+ ]
175
+
176
+
177
+ @server.call_tool()
178
+ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
179
+ """
180
+ Handle tool execution requests.
181
+ """
182
+ if name == "calculate_utility":
183
+ user_id = arguments["user_id"]
184
+
185
+ # Get user coefficients
186
+ coeffs = get_user_coefficients(user_id)
187
+
188
+ # Extract car features
189
+ car_features = {
190
+ "price": arguments["price"],
191
+ "range": arguments["range"],
192
+ "efficiency": arguments["efficiency"],
193
+ "acceleration": arguments["acceleration"],
194
+ "fast_charge": arguments["fast_charge"],
195
+ "seat_count": arguments["seat_count"]
196
+ }
197
+
198
+ # Calculate utility
199
+ utility = calculate_utility_score(car_features, coeffs)
200
+
201
+ result = {
202
+ "user_id": user_id,
203
+ "utility": utility,
204
+ "coefficients_used": coeffs,
205
+ "car_features": car_features
206
+ }
207
+
208
+ return [TextContent(
209
+ type="text",
210
+ text=json.dumps(result, indent=2)
211
+ )]
212
+
213
+ elif name == "find_best_car":
214
+ user_id = arguments["user_id"]
215
+ cars = arguments["cars"]
216
+
217
+ # Get user coefficients
218
+ coeffs = get_user_coefficients(user_id)
219
+
220
+ # Calculate utility for each car
221
+ best_car = None
222
+ best_utility = float('-inf')
223
+ all_results = []
224
+
225
+ for car in cars:
226
+ utility = calculate_utility_score(car, coeffs)
227
+ car_result = {**car, "utility": utility}
228
+ all_results.append(car_result)
229
+
230
+ if utility > best_utility:
231
+ best_utility = utility
232
+ best_car = car_result
233
+
234
+ result = {
235
+ "user_id": user_id,
236
+ "best_car": best_car,
237
+ "all_cars_with_utilities": all_results,
238
+ "coefficients_used": coeffs
239
+ }
240
+
241
+ return [TextContent(
242
+ type="text",
243
+ text=json.dumps(result, indent=2)
244
+ )]
245
+
246
+ else:
247
+ raise ValueError(f"Unknown tool: {name}")
248
+
249
+
250
+ async def main():
251
+ """
252
+ Main entry point for the MCP server.
253
+ """
254
+ async with stdio_server() as (read_stream, write_stream):
255
+ await server.run(
256
+ read_stream,
257
+ write_stream,
258
+ InitializationOptions(
259
+ server_name="utility-function",
260
+ server_version="0.1.0",
261
+ capabilities=server.get_capabilities(
262
+ notification_options=NotificationOptions(),
263
+ experimental_capabilities={},
264
+ )
265
+ )
266
+ )
267
+
268
+
269
+ if __name__ == "__main__":
270
+ import asyncio
271
+ asyncio.run(main())
test_local.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple test script to verify the utility calculation functions work correctly.
4
+ This doesn't test the MCP protocol itself, just the core logic.
5
+ """
6
+
7
+ import json
8
+ from server import get_user_coefficients, calculate_utility_score
9
+
10
+ def test_calculate_utility():
11
+ """Test basic utility calculation."""
12
+ print("Testing utility calculation...")
13
+
14
+ # Test with default coefficients
15
+ user_id = "test_user_nonexistent"
16
+ coeffs = get_user_coefficients(user_id)
17
+ print(f"\nCoefficients for {user_id}:")
18
+ print(json.dumps(coeffs, indent=2))
19
+
20
+ # Example car features
21
+ car = {
22
+ "price": 45000,
23
+ "range": 500,
24
+ "efficiency": 150,
25
+ "acceleration": 6.1,
26
+ "fast_charge": 170,
27
+ "seat_count": 5
28
+ }
29
+
30
+ utility = calculate_utility_score(car, coeffs)
31
+ print(f"\nCar features:")
32
+ print(json.dumps(car, indent=2))
33
+ print(f"\nCalculated utility: {utility:.4f}")
34
+
35
+
36
+ def test_with_saved_user():
37
+ """Test with a user that has saved preferences."""
38
+ print("\n" + "="*60)
39
+ print("Testing with saved user preferences...")
40
+
41
+ user_id = "benjo" # Should exist in Redis from the screenshot
42
+ coeffs = get_user_coefficients(user_id)
43
+ print(f"\nCoefficients for {user_id}:")
44
+ print(json.dumps({k: round(v, 4) for k, v in coeffs.items()}, indent=2))
45
+
46
+ # Example cars
47
+ cars = [
48
+ {
49
+ "name": "Tesla Model 3",
50
+ "price": 45000,
51
+ "range": 500,
52
+ "efficiency": 150,
53
+ "acceleration": 6.1,
54
+ "fast_charge": 170,
55
+ "seat_count": 5
56
+ },
57
+ {
58
+ "name": "Volkswagen ID.4",
59
+ "price": 40000,
60
+ "range": 420,
61
+ "efficiency": 180,
62
+ "acceleration": 8.5,
63
+ "fast_charge": 125,
64
+ "seat_count": 5
65
+ },
66
+ {
67
+ "name": "Hyundai Ioniq 5",
68
+ "price": 48000,
69
+ "range": 480,
70
+ "efficiency": 165,
71
+ "acceleration": 7.4,
72
+ "fast_charge": 220,
73
+ "seat_count": 5
74
+ }
75
+ ]
76
+
77
+ print(f"\nComparing {len(cars)} cars:")
78
+ results = []
79
+ for car in cars:
80
+ utility = calculate_utility_score(car, coeffs)
81
+ results.append((car["name"], utility))
82
+ print(f" {car['name']}: {utility:.4f}")
83
+
84
+ # Find best
85
+ best_car, best_utility = max(results, key=lambda x: x[1])
86
+ print(f"\n🏆 Best car for {user_id}: {best_car} (utility: {best_utility:.4f})")
87
+
88
+
89
+ if __name__ == "__main__":
90
+ try:
91
+ test_calculate_utility()
92
+ test_with_saved_user()
93
+ print("\n" + "="*60)
94
+ print("✅ All tests completed successfully!")
95
+ except Exception as e:
96
+ print(f"\n❌ Error: {e}")
97
+ import traceback
98
+ traceback.print_exc()