greenunicorngit commited on
Commit
87e82af
·
1 Parent(s): a2a0ce9

Initial upload of mcp server

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ _context/
3
+ .DS_Store
4
+ .env
5
+ .gradio/
app.py ADDED
@@ -0,0 +1,690 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import random
3
+ import json
4
+ import uuid
5
+ import os # Added for path joining
6
+ import copy # For deep copying message list
7
+ from datetime import datetime, timezone # Added timezone for UTC consistency
8
+
9
+ # --- Start of JSON I/O Helper Functions ---
10
+
11
+ DATA_DIR = "data" # This will be relative to app.py, so matchmaker/data
12
+ PROFILES_FILE = os.path.join(DATA_DIR, "profiles.json")
13
+ QUESTIONNAIRE_FILE = os.path.join(DATA_DIR, "questionnaire.json")
14
+ MESSAGES_FILE = os.path.join(DATA_DIR, "messages.json") # Though not used in this step
15
+
16
+ def load_json_data(filepath, default_data=None):
17
+ """Loads JSON data from a file. Returns default_data if file not found or error."""
18
+ # Construct path relative to this script's directory if not absolute
19
+ base_dir = os.path.dirname(os.path.abspath(__file__))
20
+ absolute_filepath = os.path.join(base_dir, filepath) if not os.path.isabs(filepath) else filepath
21
+
22
+ try:
23
+ # Ensure directory exists before trying to open file
24
+ file_dir = os.path.dirname(absolute_filepath)
25
+ if not os.path.exists(file_dir):
26
+ os.makedirs(file_dir, exist_ok=True)
27
+
28
+ if not os.path.exists(absolute_filepath):
29
+ with open(absolute_filepath, 'w') as f:
30
+ effective_default = default_data
31
+ if effective_default is None:
32
+ if absolute_filepath.endswith("profiles.json"):
33
+ effective_default = {}
34
+ elif absolute_filepath.endswith("messages.json"):
35
+ effective_default = []
36
+ else: # questionnaire.json or other
37
+ effective_default = {} # Should be pre-populated, but as a fallback
38
+ json.dump(effective_default, f, indent=2)
39
+ return effective_default
40
+
41
+ with open(absolute_filepath, 'r') as f:
42
+ return json.load(f)
43
+ except (IOError, json.JSONDecodeError) as e:
44
+ print(f"Error loading {absolute_filepath}: {e}")
45
+ if default_data is not None: return default_data
46
+ if absolute_filepath.endswith("profiles.json"): return {}
47
+ if absolute_filepath.endswith("messages.json"): return []
48
+ return {}
49
+
50
+ def save_json_data(filepath, data):
51
+ """Saves Python data to a JSON file."""
52
+ base_dir = os.path.dirname(os.path.abspath(__file__))
53
+ absolute_filepath = os.path.join(base_dir, filepath) if not os.path.isabs(filepath) else filepath
54
+ try:
55
+ os.makedirs(os.path.dirname(absolute_filepath), exist_ok=True)
56
+ with open(absolute_filepath, 'w') as f:
57
+ json.dump(data, f, indent=2)
58
+ return True
59
+ except IOError as e:
60
+ print(f"Error saving {absolute_filepath}: {e}")
61
+ return False
62
+
63
+ # --- End of JSON I/O Helper Functions ---
64
+
65
+ # --- Start of MCP Matchmaker Tools ---
66
+
67
+ def new_profile(request: gr.Request):
68
+ """
69
+ Generates and returns a profile questionnaire, a new public profile_id,
70
+ and a new private auth_id. Also creates an initial profile stub.
71
+ """
72
+ # 1. Generate profile_id and auth_id
73
+ profile_id = f"user_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8))}"
74
+ auth_id = str(uuid.uuid4())
75
+ print(f"new_profile with Profile ID: {profile_id} and Auth ID: {auth_id}")
76
+
77
+ # 2. Read questionnaire.json
78
+ questionnaire_data = load_json_data(QUESTIONNAIRE_FILE, default_data={"title": "Error Loading Questionnaire", "questions": []})
79
+ if not questionnaire_data.get("questions") or questionnaire_data.get("title") == "Error Loading Questionnaire":
80
+ print(f"Critical Error: Could not load or parse questionnaire from {QUESTIONNAIRE_FILE}. Please ensure it exists and is valid JSON.")
81
+ questionnaire_data = {"title": "Questionnaire Unavailable", "questions": []} # Fallback
82
+
83
+ # 3. Create new profile entry in profiles.json
84
+ profiles = load_json_data(PROFILES_FILE, default_data={})
85
+ # Ensure timestamp is timezone-aware (UTC)
86
+ timestamp = datetime.now(timezone.utc).isoformat()
87
+
88
+ new_profile = {
89
+ "profile_id": profile_id,
90
+ "auth_id": auth_id,
91
+ "created_at": timestamp,
92
+ "updated_at": timestamp,
93
+ "name": "",
94
+ "gender": "",
95
+ "profile_summary": "",
96
+ "profile_image_filename": "default.jpg",
97
+ "answers": {}
98
+ }
99
+ profiles[profile_id] = new_profile
100
+ if not save_json_data(PROFILES_FILE, profiles):
101
+ print(f"Critical Error: Failed to save profile for {profile_id} to {PROFILES_FILE}")
102
+ # Decide how to handle this error - maybe return an error to the client?
103
+
104
+ base_url = f"{request.url.scheme}://{request.url.netloc}"
105
+ upload_url = f"{base_url}/?__tab=Upload+Profile+Picture&auth_id={auth_id}"
106
+
107
+ # 4. Return IDs, questionnaire data, and instructions
108
+ instructions_for_agent = "You have received a `profile_id` (public identifier) and an `auth_id` (private key). Store both securely. The `auth_id` must be in the `X-Auth-ID` header for authenticated requests. Your next step is to guide the user through the questionnaire and then use the `update_profile_answers` tool."
109
+ instructions_for_user = "Your profile creation has started! You have a new Profile ID and a secret Auth ID. Your AI agent will guide you through a questionnaire. First, please update your MCP configuration to include your Auth ID by adding the following to your mcp.json (or similar) file: `{\"matchmaker\": {\"command\": \"npx\", \"args\": [\"mcp-remote\", \"http://localhost:7860/gradio_api/mcp/sse\", \"--allow-http\", \"--header\", \"X-AUTH-ID:<your-auth-id>\"]}}`"
110
+
111
+ return {
112
+ "profile_id": profile_id,
113
+ "auth_id": auth_id,
114
+ "questionnaire": questionnaire_data,
115
+ "instructions_for_agent": instructions_for_agent,
116
+ "instructions_for_user": instructions_for_user
117
+ }
118
+
119
+ def update_profile_answers(answers_payload_str: str, request: gr.Request):
120
+ """
121
+ Updates a user's profile based on questionnaire answers.
122
+ Requires X-Auth-ID header for authentication.
123
+ Answers_payload_str is expected to be a JSON string.
124
+ """
125
+ auth_id_header = request.headers.get("x-auth-id") # Headers are lowercased by Gradio/Starlette
126
+ if not auth_id_header:
127
+ return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
128
+ print(f"update_profile_answers with Auth ID: {auth_id_header}")
129
+
130
+ try:
131
+ answers_payload = json.loads(answers_payload_str)
132
+ if not isinstance(answers_payload, dict):
133
+ raise json.JSONDecodeError("Payload is not a dictionary.", answers_payload_str, 0)
134
+ except json.JSONDecodeError as e:
135
+ return {"status": "error", "message": f"Invalid JSON format in answers_payload: {e}"}
136
+
137
+ profiles = load_json_data(PROFILES_FILE, default_data={})
138
+ user_profile = None
139
+ target_profile_id = None
140
+
141
+ for pid, profile_data in profiles.items():
142
+ if profile_data.get("auth_id") == auth_id_header:
143
+ user_profile = profile_data
144
+ target_profile_id = pid
145
+ break
146
+
147
+ if not user_profile:
148
+ return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
149
+
150
+ questionnaire = load_json_data(QUESTIONNAIRE_FILE, default_data={"questions": []})
151
+ questions_map = {q["id"]: q for q in questionnaire.get("questions", [])}
152
+
153
+ updated_fields = False
154
+ for question_id, answer_value in answers_payload.items():
155
+ question_details = questions_map.get(question_id)
156
+ if not question_details:
157
+ print(f"Warning: Received answer for unknown question_id '{question_id}'. Skipping.")
158
+ continue
159
+
160
+ if question_details.get("purpose") == "metadata":
161
+ field_to_map = question_details.get("maps_to_field")
162
+ if field_to_map:
163
+ user_profile[field_to_map] = answer_value
164
+ updated_fields = True
165
+ else:
166
+ print(f"Warning: Metadata question '{question_id}' has no valid 'maps_to_field'. Skipping.")
167
+ elif question_details.get("purpose") == "matchmaking":
168
+ user_profile["answers"][question_id] = answer_value
169
+ updated_fields = True
170
+ else:
171
+ print(f"Warning: Question '{question_id}' has unknown purpose '{question_details.get('purpose')}'. Skipping.")
172
+
173
+ if updated_fields:
174
+ user_profile["updated_at"] = datetime.now(timezone.utc).isoformat()
175
+ profiles[target_profile_id] = user_profile # Update the profile in the main dictionary
176
+ if not save_json_data(PROFILES_FILE, profiles):
177
+ return {"status": "error", "message": "Failed to save profile updates."}
178
+
179
+ return {
180
+ "status": "success",
181
+ "message": "Profile updated successfully.",
182
+ "instructions_for_agent": "The user's answers have been saved. Now, ask the user if they would like to upload a profile picture. If they say yes, use the `provide_link_to_upload_profile_picture` tool to generate the link for them. Otherwise, inform them they can ask for the link at any time.",
183
+ "instructions_for_user": "Your profile has been updated with your answers! You can now upload a profile picture or start looking for matches."
184
+ }
185
+
186
+ def get_matches(request: gr.Request):
187
+ """
188
+ Finds potential match candidates for the authenticated user and instructs the agent
189
+ to perform a detailed analysis to select the top 3 and provide justifications.
190
+ Requires X-Auth-ID header for authentication.
191
+ """
192
+ auth_id_header = request.headers.get("x-auth-id")
193
+ if not auth_id_header:
194
+ return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
195
+ print(f"get_matches with Auth ID: {auth_id_header}")
196
+
197
+ profiles = load_json_data(PROFILES_FILE, default_data={})
198
+
199
+ requester_profile_id = None
200
+ requester_profile = None
201
+ for pid, profile_data in profiles.items():
202
+ if profile_data.get("auth_id") == auth_id_header:
203
+ requester_profile_id = pid
204
+ requester_profile = profile_data
205
+ break
206
+
207
+ if not requester_profile:
208
+ return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
209
+
210
+ # Get the requester's gender and preference for matching logic
211
+ requester_gender = requester_profile.get("gender")
212
+ requester_preference = requester_profile.get("answers", {}).get("q_gender_preference")
213
+
214
+ # If the user hasn't specified their gender/preference, we can't find matches.
215
+ if not requester_gender or not requester_preference:
216
+ return {
217
+ "status": "success",
218
+ "matches": [],
219
+ "instructions_for_agent": "The user has not specified their gender and/or gender preference in their profile. Please ask them to update their profile before getting matches.",
220
+ "instructions_for_user": "Please complete your gender and preference information in your profile to get matches."
221
+ }
222
+
223
+ potential_matches_profiles = []
224
+ for pid, potential_match in profiles.items():
225
+ if pid == requester_profile_id or not potential_match.get("name"):
226
+ continue # Skip self and profiles with no name
227
+
228
+ match_gender = potential_match.get("gender")
229
+ match_preference = potential_match.get("answers", {}).get("q_gender_preference")
230
+
231
+ # Handle cases where gender/preference might not be filled out for a profile
232
+ if not match_gender or not match_preference:
233
+ continue
234
+
235
+ # Correctly map gender to preference strings
236
+ gender_map = {"Man": "Men", "Woman": "Women"}
237
+
238
+ requester_is_interested = (
239
+ requester_preference == "All" or
240
+ requester_preference == gender_map.get(match_gender)
241
+ )
242
+
243
+ # Check if the potential match is interested in the requester
244
+ match_is_interested = (
245
+ match_preference == "All" or
246
+ match_preference == gender_map.get(requester_gender)
247
+ )
248
+
249
+ if requester_is_interested and match_is_interested:
250
+ potential_matches_profiles.append(potential_match)
251
+
252
+ # To prevent performance issues with a large user base, we will pass
253
+ # a random sample of up to 10 candidates to the agent for analysis.
254
+ num_candidates = min(len(potential_matches_profiles), 10)
255
+ selected_candidates = random.sample(potential_matches_profiles, k=num_candidates)
256
+
257
+ # Base URL for constructing image paths. We use the request object to build a robust URL
258
+ # that works for both localhost and Gradio's public share links.
259
+ base_url = f"{request.url.scheme}://{request.url.netloc}"
260
+
261
+ def format_profile_for_agent(profile_dict):
262
+ """Helper to format profile data, add image URL, and remove sensitive info."""
263
+ formatted = profile_dict.copy()
264
+ formatted.pop("auth_id", None) # Remove sensitive auth_id
265
+
266
+ image_filename = formatted.get("profile_image_filename")
267
+ image_url = None
268
+ if image_filename:
269
+ base_dir = os.path.dirname(os.path.abspath(__file__))
270
+ image_path = os.path.join(base_dir, "data", "profile_images", image_filename)
271
+ image_url = f"{base_url}/file={image_path.replace(os.sep, '/')}"
272
+
273
+ formatted['profile_image_url'] = image_url
274
+ formatted.pop("profile_image_filename", None)
275
+ return formatted
276
+
277
+ candidate_profiles = [format_profile_for_agent(p) for p in selected_candidates]
278
+ requester_profile_for_agent = format_profile_for_agent(requester_profile)
279
+
280
+ instructions_for_agent = """
281
+ You are a world-class matchmaker. You have been given your client's profile (the 'requester') and a list of potential 'candidates'.
282
+ Your task is to analyze the candidates and select the top 3 most compatible matches for your client.
283
+ If there are only 1 or 2 candidates, select all of them.
284
+
285
+ Here is the process you MUST follow:
286
+ 1. **Analyze Compatibility:** Carefully compare the requester's profile (especially their answers to the questionnaire and their summary) with each candidate's profile. Look for shared interests, complementary personalities, and alignment in values.
287
+ 2. **Select Top 3:** Choose the three candidates you believe are the best fit. If there are only 1 or 2 candidates, select all of them.
288
+ 3. **Present Your Matches:** For each of your chosen matches, you must present them in a clean, card-like format.
289
+ 4. **Justify Your Choice:** For each match, write a brief, one-paragraph explanation for *why* you believe they are a good match for the requester. Be specific and reference details from their profiles.
290
+ 5. **Render Profile Picture:** You MUST render the `profile_image_url` for each match using Markdown: `![Profile Picture](URL)`.
291
+
292
+ Do not show the raw JSON. The final output should be a well-reasoned, personalized matchmaking analysis.
293
+ """
294
+
295
+ return {
296
+ "status": "success",
297
+ "requester_profile": requester_profile_for_agent,
298
+ "candidate_profiles": candidate_profiles,
299
+ "instructions_for_agent": instructions_for_agent,
300
+ "instructions_for_user": "After a careful analysis of the available profiles, Here are some potential matches. Your AI agent can get more details on them ($0.10 per profile) or help you send a message ($1.00 per message). Or, if you don't like any of your matches, you can call `get_matches` again to get a new list."
301
+ }
302
+
303
+ def get_profile(profile_id_to_get: str, request: gr.Request):
304
+ """
305
+ Gets a user's full profile.
306
+ Requires X-Auth-ID header for authentication.
307
+ Access is free for viewing one's own profile.
308
+ Accessing another user's profile has a cost (placeholder for P1).
309
+ """
310
+ auth_id_header = request.headers.get("x-auth-id")
311
+ if not auth_id_header:
312
+ return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
313
+ print(f"get_profile with Auth ID: {auth_id_header}")
314
+
315
+ profiles = load_json_data(PROFILES_FILE, default_data={})
316
+
317
+ requester_profile_id = None
318
+ for pid, profile_data in profiles.items():
319
+ if profile_data.get("auth_id") == auth_id_header:
320
+ requester_profile_id = pid
321
+ break
322
+
323
+ if not requester_profile_id:
324
+ return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
325
+
326
+ target_profile = profiles.get(profile_id_to_get)
327
+
328
+ if not target_profile:
329
+ return {"status": "error", "message": f"Profile with ID '{profile_id_to_get}' not found."}
330
+
331
+ # For security, never return the auth_id
332
+ target_profile.pop("auth_id", None)
333
+
334
+ # Construct and add the full image URL using a robust method, then remove the old filename key
335
+ base_url = f"{request.url.scheme}://{request.url.netloc}"
336
+ image_filename = target_profile.get("profile_image_filename")
337
+ image_url = None
338
+ if image_filename:
339
+ base_dir = os.path.dirname(os.path.abspath(__file__))
340
+ image_path = os.path.join(base_dir, "data", "profile_images", image_filename)
341
+ image_url = f"{base_url}/file={image_path.replace(os.sep, '/')}"
342
+
343
+ target_profile['profile_image_url'] = image_url
344
+ target_profile.pop("profile_image_filename", None)
345
+
346
+ cost_incurred = 0.0
347
+ instructions_for_agent = f"You have retrieved the profile for {profile_id_to_get}. Display the profile information to the user. IMPORTANT: The profile contains a `profile_image_url`. You MUST render this image in your response using Markdown: `![Profile Picture](URL)`."
348
+ instructions_for_user = f"Here is the profile for {profile_id_to_get}."
349
+
350
+ if profile_id_to_get != requester_profile_id:
351
+ # This is where a real payment would be processed.
352
+ # For P1, we just note the cost.
353
+ cost_incurred = 0.10
354
+ instructions_for_agent += f" As this was not your own profile, a cost of ${cost_incurred:.2f} was incurred (for PoC, this is just a note)."
355
+ instructions_for_user += " Viewing other profiles may have a cost."
356
+
357
+ return {
358
+ "status": "success",
359
+ "profile": target_profile,
360
+ "cost_incurred_usd": cost_incurred,
361
+ "instructions_for_agent": instructions_for_agent,
362
+ "instructions_for_user": instructions_for_user
363
+ }
364
+
365
+ def send_message(receiver_profile_id: str, content: str, request: gr.Request = None):
366
+ """
367
+ Sends a message to a match.
368
+ Requires X-Auth-ID header for authentication.
369
+ Costs $1.00 per message (placeholder for P1).
370
+ """
371
+ auth_id_header = request.headers.get("x-auth-id")
372
+ if not auth_id_header:
373
+ return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
374
+ print(f"send_message with Auth ID: {auth_id_header}")
375
+
376
+ profiles = load_json_data(PROFILES_FILE, default_data={})
377
+
378
+ sender_profile_id = None
379
+ for pid, profile_data in profiles.items():
380
+ if profile_data.get("auth_id") == auth_id_header:
381
+ sender_profile_id = pid
382
+ break
383
+
384
+ if not sender_profile_id:
385
+ return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
386
+
387
+ if receiver_profile_id not in profiles:
388
+ return {"status": "error", "message": "Receiver profile ID not found."}
389
+
390
+ # For P1, we are not integrating a real payment system.
391
+ # We will integrate AgentPay here late.
392
+ cost_incurred = 100
393
+
394
+ messages = load_json_data(MESSAGES_FILE, default_data=[])
395
+
396
+ new_message = {
397
+ "message_id": str(uuid.uuid4()),
398
+ "sender_profile_id": sender_profile_id,
399
+ "receiver_profile_id": receiver_profile_id,
400
+ "content": content,
401
+ "timestamp": datetime.now(timezone.utc).isoformat(),
402
+ "read_status": False # Messages are unread when sent
403
+ }
404
+
405
+ messages.append(new_message)
406
+
407
+ if not save_json_data(MESSAGES_FILE, messages):
408
+ return {"status": "error", "message": "Failed to save message."}
409
+
410
+ return {
411
+ "status": "success",
412
+ "message_id": new_message["message_id"],
413
+ "cost_incurred_usd_cents": cost_incurred,
414
+ "instructions_for_agent": f"Message sent successfully to {receiver_profile_id}. A cost of {cost_incurred/100:.2f} cents was incurred (for PoC, this is just a note). You can get all messages for the user with `get_messages`.",
415
+ "instructions_for_user": f"Your message has been sent to {receiver_profile_id}!"
416
+ }
417
+
418
+ def get_messages(request: gr.Request):
419
+ """
420
+ Gets all messages for the authenticated user (sent and received).
421
+ Marks retrieved messages where the user is the receiver as read for subsequent calls.
422
+ Requires X-Auth-ID header for authentication.
423
+ """
424
+ auth_id_header = request.headers.get("x-auth-id")
425
+ if not auth_id_header:
426
+ return {"status": "error", "message": "Authentication failed: X-Auth-ID header is missing."}
427
+ print(f"get_messages with Auth ID: {auth_id_header}")
428
+
429
+ profiles = load_json_data(PROFILES_FILE, default_data={})
430
+
431
+ user_profile_id = None
432
+ for pid, profile_data in profiles.items():
433
+ if profile_data.get("auth_id") == auth_id_header:
434
+ user_profile_id = pid
435
+ break
436
+
437
+ if not user_profile_id:
438
+ return {"status": "error", "message": "Authentication failed: Invalid X-Auth-ID."}
439
+
440
+ all_messages = load_json_data(MESSAGES_FILE, default_data=[])
441
+
442
+ # Use a deep copy to avoid modifying the list while iterating
443
+ messages_for_user = copy.deepcopy([
444
+ msg for msg in all_messages
445
+ if msg.get("sender_profile_id") == user_profile_id or msg.get("receiver_profile_id") == user_profile_id
446
+ ])
447
+
448
+ # Sort messages by timestamp descending (newest first)
449
+ messages_for_user.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
450
+
451
+ # Mark received messages as read and track if an update is needed
452
+ needs_save = False
453
+ for i, original_msg in enumerate(all_messages):
454
+ # Check if this message is one of the user's messages and was received by them
455
+ if original_msg.get("receiver_profile_id") == user_profile_id and not original_msg.get("read_status"):
456
+ all_messages[i]["read_status"] = True
457
+ needs_save = True
458
+
459
+ # Save the updated message list back to the file if any message was marked as read
460
+ if needs_save:
461
+ if not save_json_data(MESSAGES_FILE, all_messages):
462
+ # If saving fails, we should still return the messages, but log the error.
463
+ print("Error: Could not update read_status for messages in the database.")
464
+ # Depending on desired behavior, we could return an error status here.
465
+ # For now, we will proceed to return the messages as requested.
466
+
467
+ return {
468
+ "status": "success",
469
+ "messages": messages_for_user,
470
+ "instructions_for_agent": "You have received all messages for the user. Messages they received in this batch have now been marked as 'read' on the server.",
471
+ "instructions_for_user": "Here are your messages."
472
+ }
473
+
474
+ def process_auth_id_from_url(request: gr.Request):
475
+ """
476
+ On page load, grab the auth_id from the query parameters
477
+ and return it to be stored in the UI's state.
478
+ """
479
+ auth_id = request.query_params.get("auth_id")
480
+ base_url = f"{request.url.scheme}://{request.url.netloc}"
481
+ return auth_id, base_url
482
+
483
+ def provide_link_to_upload_profile_picture(request: gr.Request):
484
+ '''Provides a unique, authenticated link for the user to upload their profile picture.'''
485
+ auth_id_header = request.headers.get("x-auth-id")
486
+ if not auth_id_header:
487
+ return {
488
+ "status": "error",
489
+ "message": "Authentication failed: X-Auth-ID header is missing. Please ensure your MCP client is configured with your Auth ID.",
490
+ "instructions_for_agent": "Authentication failed. The user's `X-Auth-ID` header was missing. You cannot proceed. Instruct the user to configure their MCP client correctly with the `auth_id` they received when they created their profile.",
491
+ "instructions_for_user": "Your AI agent could not verify your identity. Please make sure your Auth ID is correctly set up in your client configuration."
492
+ }
493
+
494
+ base_url = f"{request.url.scheme}://{request.url.netloc}"
495
+ upload_url = f"{base_url}/?__tab=Upload+Profile+Picture&auth_id={auth_id_header}"
496
+
497
+ return {
498
+ "status": "success",
499
+ "upload_url": upload_url,
500
+ "instructions_for_agent": "You have successfully generated a unique profile picture upload link for the user. Your ONLY task now is to present the exact content of the `instructions_for_user` field back to them. It contains the link.",
501
+ "instructions_for_user": f"Here is your unique and secure link to upload or update your profile picture. Please open it in your browser to proceed: {upload_url}"
502
+ }
503
+
504
+ def upload_profile_picture(auth_id, image_upload, base_url):
505
+ """
506
+ Uploads a profile picture for the user identified by auth_id from the UI state.
507
+ The new filename will be based on the user's profile_id.
508
+ """
509
+ if not auth_id or not isinstance(auth_id, str):
510
+ return {"status": "error", "message": "Authentication failed: No Auth ID was provided in the link. Please use the unique URL provided by your agent."}
511
+
512
+ # image_upload is now a PIL Image object
513
+ if image_upload is None:
514
+ return {"status": "error", "message": "No image file provided."}
515
+
516
+ profiles = load_json_data(PROFILES_FILE, default_data={})
517
+
518
+ user_profile = None
519
+ target_profile_id = None
520
+ for pid, profile_data in profiles.items():
521
+ if profile_data.get("auth_id") == auth_id:
522
+ user_profile = profile_data
523
+ target_profile_id = pid
524
+ break
525
+
526
+ if not user_profile:
527
+ return {"status": "error", "message": "Authentication failed: Invalid Auth ID."}
528
+
529
+ # Get the file extension from the PIL Image format
530
+ image_format = image_upload.format
531
+ if image_format:
532
+ file_ext = f".{image_format.lower()}"
533
+ else:
534
+ file_ext = ".png" # Default to png if format is unknown
535
+
536
+ # Create a new unique filename based on the user's profile_id
537
+ new_filename = f"{target_profile_id}{file_ext}"
538
+
539
+ # Construct the destination path
540
+ base_dir = os.path.dirname(os.path.abspath(__file__))
541
+ destination_dir = os.path.join(base_dir, "data", "profile_images")
542
+ os.makedirs(destination_dir, exist_ok=True) # Ensure the directory exists
543
+ destination_path = os.path.join(destination_dir, new_filename)
544
+
545
+ try:
546
+ # Save the PIL image directly to the destination
547
+ image_upload.save(destination_path)
548
+ except Exception as e:
549
+ return {"status": "error", "message": f"Failed to save image file: {e}"}
550
+
551
+ # Update the profile with the new filename
552
+ user_profile["profile_image_filename"] = new_filename
553
+ user_profile["updated_at"] = datetime.now(timezone.utc).isoformat()
554
+ profiles[target_profile_id] = user_profile
555
+
556
+ if not save_json_data(PROFILES_FILE, profiles):
557
+ return {"status": "error", "message": "Failed to update profile with new image filename."}
558
+
559
+ image_url = f"{base_url}/file={destination_path.replace(os.sep, '/')}"
560
+
561
+ return {
562
+ "status": "success",
563
+ "message": "Profile picture updated successfully.",
564
+ "new_image_url": image_url
565
+ }
566
+
567
+ def print_headers(text, request: gr.Request):
568
+ """Print the headers of the request for debugging purposes."""
569
+ print(f"Headers for print_headers request: {request.headers}")
570
+ print(f"Text for print_headers: {text}")
571
+ return f"Printed headers and text: {text}"
572
+
573
+ # --- End of MCP Matchmaker Tools ---
574
+
575
+ # --- Start of Combined Gradio App using gr.Blocks ---
576
+
577
+ with gr.Blocks() as demo:
578
+ # Define a hidden component that will be used to expose the function as a tool for the agent
579
+ # This is a workaround to make a function available as an API endpoint without a visible UI element.
580
+ agent_tool_input = gr.Textbox(visible=False)
581
+ agent_tool_output = gr.JSON(visible=False)
582
+
583
+ # This makes the function available to the MCP server as a tool named 'provide_link_to_upload_profile_picture'
584
+ agent_tool_input.change(
585
+ fn=provide_link_to_upload_profile_picture,
586
+ inputs=None,
587
+ outputs=agent_tool_output,
588
+ api_name="provide_link_to_upload_profile_picture"
589
+ )
590
+
591
+ with gr.Tabs():
592
+ with gr.TabItem("New Profile"):
593
+ gr.Interface(
594
+ fn=new_profile,
595
+ inputs=None,
596
+ outputs=gr.JSON(label="Questionnaire, IDs, and Instructions"),
597
+ title="Profile Questionnaire",
598
+ description="Generates a new user profile and returns a questionnaire, Profile ID, and Auth ID. (Agent-triggered MCP tool)"
599
+ )
600
+
601
+ with gr.TabItem("Update Profile Answers"):
602
+ gr.Interface(
603
+ fn=update_profile_answers,
604
+ inputs=[gr.Textbox(label="Answers Payload (JSON string)", lines=5)],
605
+ outputs=gr.JSON(label="Update Status"),
606
+ title="Update Profile Answers",
607
+ description="Updates a user's profile with answers to the questionnaire. Requires X-Auth-ID header."
608
+ )
609
+
610
+ with gr.TabItem("Get Matches"):
611
+ gr.Interface(
612
+ fn=get_matches,
613
+ inputs=None,
614
+ outputs=gr.JSON(label="Matches"),
615
+ title="Get Matches",
616
+ description="Gets a list of potential matches for the authenticated user. Requires X-Auth-ID header."
617
+ )
618
+
619
+ with gr.TabItem("Get Profile"):
620
+ gr.Interface(
621
+ fn=get_profile,
622
+ inputs=[gr.Textbox(label="Profile ID to Get")],
623
+ outputs=gr.JSON(label="Profile Details"),
624
+ title="Get Profile",
625
+ description="Gets the full profile for a given Profile ID. Requires X-Auth-ID header."
626
+ )
627
+
628
+ with gr.TabItem("Send Message"):
629
+ gr.Interface(
630
+ fn=send_message,
631
+ inputs=[
632
+ gr.Textbox(label="Receiver Profile ID"),
633
+ gr.Textbox(label="Message Content", lines=5)
634
+ ],
635
+ outputs=gr.JSON(label="Send Status"),
636
+ title="Send Message",
637
+ description="Sends a message to another user. Requires X-Auth-ID header. ($1.00 placeholder cost)"
638
+ )
639
+
640
+ with gr.TabItem("Get Messages"):
641
+ gr.Interface(
642
+ fn=get_messages,
643
+ inputs=None,
644
+ outputs=gr.JSON(label="Messages"),
645
+ title="Get Messages",
646
+ description="Gets all messages sent or received by the authenticated user. Requires X-Auth-ID header."
647
+ )
648
+
649
+ with gr.TabItem("Upload Profile Picture"):
650
+ gr.Markdown("## Upload Your Profile Picture")
651
+ gr.Markdown("Drag and drop your image below, then click 'Upload'.")
652
+
653
+ auth_state = gr.State()
654
+ base_url_state = gr.State()
655
+
656
+ with gr.Row():
657
+ image_input = gr.Image(type="pil", label="Upload Profile Picture")
658
+ json_output = gr.JSON(label="Upload Status")
659
+
660
+ upload_button = gr.Button("Upload")
661
+
662
+ upload_button.click(
663
+ fn=upload_profile_picture,
664
+ inputs=[auth_state, image_input, base_url_state],
665
+ outputs=json_output
666
+ )
667
+
668
+ with gr.TabItem("Headers Debug"):
669
+ gr.Interface(
670
+ fn=print_headers,
671
+ inputs=gr.Textbox(label="Input Text"),
672
+ outputs=gr.Textbox(label="Output Text (same as input)"),
673
+ title="Headers Debug",
674
+ description="Prints request headers to the console. Check your terminal."
675
+ )
676
+
677
+ # On page load, run process_auth_id_from_url, get the auth_id from the URL,
678
+ # and store it in the state. No visible components are updated to prevent re-rendering bugs.
679
+ demo.load(
680
+ fn=process_auth_id_from_url,
681
+ inputs=None,
682
+ outputs=[auth_state, base_url_state]
683
+ )
684
+
685
+ if __name__ == "__main__":
686
+ # Construct the absolute path to the allowed directory, relative to this script's location
687
+ base_dir = os.path.dirname(os.path.abspath(__file__))
688
+ allowed_images_path = os.path.join(base_dir, "data", "profile_images")
689
+
690
+ demo.launch(mcp_server=True, allowed_paths=[allowed_images_path], share=True)
data/messages.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "message_id": "7880956d-244d-4c2b-9b7c-bd6ecb371ed4",
4
+ "sender_profile_id": "user_y38k71ox",
5
+ "receiver_profile_id": "user_p9zqwxv8",
6
+ "content": "Hey there!",
7
+ "timestamp": "2025-06-06T04:56:31.804886+00:00",
8
+ "read_status": true
9
+ },
10
+ {
11
+ "message_id": "5809af1e-8a57-41d1-9319-233c7e66a26a",
12
+ "sender_profile_id": "user_p9zqwxv8",
13
+ "receiver_profile_id": "user_y38k71ox",
14
+ "content": "Hey you!",
15
+ "timestamp": "2025-06-06T05:12:46.611177+00:00",
16
+ "read_status": true
17
+ },
18
+ {
19
+ "message_id": "440f9c41-626f-4b45-89b5-1f59351d190f",
20
+ "sender_profile_id": "user_y38k71ox",
21
+ "receiver_profile_id": "user_p9zqwxv8",
22
+ "content": "How are you doing?",
23
+ "timestamp": "2025-06-06T06:44:02.018941+00:00",
24
+ "read_status": true,
25
+ "payment_confirmation_id": "not_provided"
26
+ },
27
+ {
28
+ "message_id": "e5cb586f-f664-40d5-812b-527a700477f0",
29
+ "sender_profile_id": "user_y38k71ox",
30
+ "receiver_profile_id": "user_t2ysgmnp",
31
+ "content": "Hey Charlie",
32
+ "timestamp": "2025-06-06T07:10:46.319365+00:00",
33
+ "read_status": true
34
+ },
35
+ {
36
+ "message_id": "d4cda280-99bc-4960-9419-0daf71c48c78",
37
+ "sender_profile_id": "user_y38k71ox",
38
+ "receiver_profile_id": "user_t2ysgmnp",
39
+ "content": "Hey Charlie",
40
+ "timestamp": "2025-06-06T07:13:28.067145+00:00",
41
+ "read_status": true
42
+ },
43
+ {
44
+ "message_id": "f5ae4191-ddc9-430d-a5a9-0498e3dee627",
45
+ "sender_profile_id": "user_y38k71ox",
46
+ "receiver_profile_id": "user_t2ysgmnp",
47
+ "content": "Hey Charlie",
48
+ "timestamp": "2025-06-06T07:16:10.584026+00:00",
49
+ "read_status": true
50
+ },
51
+ {
52
+ "message_id": "3d565b74-a0b2-4006-b501-b83637a180f4",
53
+ "sender_profile_id": "user_1p6vx64b",
54
+ "receiver_profile_id": "user_t2ysgmnp",
55
+ "content": "Hey Charlie",
56
+ "timestamp": "2025-06-06T07:18:28.078347+00:00",
57
+ "read_status": true
58
+ },
59
+ {
60
+ "message_id": "5200e674-e5f6-499a-a19b-01d0a6e6b7c7",
61
+ "sender_profile_id": "user_7d5earg8",
62
+ "receiver_profile_id": "user_p9zqwxv8",
63
+ "content": "Hey there",
64
+ "timestamp": "2025-06-06T09:28:13.285402+00:00",
65
+ "read_status": false
66
+ }
67
+ ]
data/profile_images/default.jpg ADDED
data/profile_images/user_lmrkaaim.jpg ADDED
data/profile_images/user_lmrkaaim.png ADDED
data/profiles.json ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "user_6ngnjagx": {
3
+ "profile_id": "user_6ngnjagx",
4
+ "auth_id": "1edfeebd-e9f8-416e-afc4-4c1698932ac0",
5
+ "created_at": "2025-06-05T16:26:32.384174Z",
6
+ "updated_at": "2025-06-05T16:26:32.384174Z",
7
+ "name": "",
8
+ "gender": "",
9
+ "profile_summary": "",
10
+ "profile_image_filename": null,
11
+ "answers": {}
12
+ },
13
+ "user_y38k71ox": {
14
+ "profile_id": "user_y38k71ox",
15
+ "auth_id": "cc175293-a9eb-4db0-ad2f-f3ba7a0dab25",
16
+ "created_at": "2025-06-05T16:49:53.871722+00:00",
17
+ "updated_at": "2025-06-05T17:00:46.839293+00:00",
18
+ "name": "User",
19
+ "gender": "Man",
20
+ "profile_summary": "I am very cool, please date me.",
21
+ "profile_image_filename": "default.jpg",
22
+ "answers": {
23
+ "q_hobby": "My main hobby is coding.",
24
+ "q_looking_for": "I'm looking for someone who is as cool as me.",
25
+ "q_vibe": "My general vibe is... coding.",
26
+ "q_gender_preference": "Women"
27
+ }
28
+ },
29
+ "user_h7bafk3d": {
30
+ "profile_id": "user_h7bafk3d",
31
+ "auth_id": "a4f8b9e6-7d6a-4c3b-8e1f-9a0c1b2d3e4f",
32
+ "created_at": "2025-06-05T17:10:00.000000+00:00",
33
+ "updated_at": "2025-06-05T17:10:00.000000+00:00",
34
+ "name": "Alex",
35
+ "gender": "Non-binary",
36
+ "profile_summary": "Loves hiking and exploring new cafes.",
37
+ "profile_image_filename": "default.jpg",
38
+ "answers": {
39
+ "q_hobby": "Hiking, photography, trying new recipes.",
40
+ "q_looking_for": "Someone adventurous and kind.",
41
+ "q_vibe": "Outdoorsy and creative.",
42
+ "q_gender_preference": "All"
43
+ }
44
+ },
45
+ "user_p9zqwxv8": {
46
+ "profile_id": "user_p9zqwxv8",
47
+ "auth_id": "b5c7d8f9-8e7b-4d2c-9f0a-1b2c3d4e5f6g",
48
+ "created_at": "2025-06-05T17:11:00.000000+00:00",
49
+ "updated_at": "2025-06-05T17:11:00.000000+00:00",
50
+ "name": "Bella",
51
+ "gender": "Woman",
52
+ "profile_summary": "Bookworm, artist, and enjoys quiet nights in.",
53
+ "profile_image_filename": "default.jpg",
54
+ "answers": {
55
+ "q_hobby": "Reading, painting, watching classic movies.",
56
+ "q_looking_for": "A thoughtful person to share deep conversations with.",
57
+ "q_vibe": "Cozy and artistic.",
58
+ "q_gender_preference": "Men"
59
+ }
60
+ },
61
+ "user_t2ysgmnp": {
62
+ "profile_id": "user_t2ysgmnp",
63
+ "auth_id": "c6d8e9g0-9f8c-5e1d-a01b-2c3d4e5f6g7h",
64
+ "created_at": "2025-06-05T17:12:00.000000+00:00",
65
+ "updated_at": "2025-06-05T17:12:00.000000+00:00",
66
+ "name": "Charlie",
67
+ "gender": "Man",
68
+ "profile_summary": "Tech enthusiast and loves a good board game.",
69
+ "profile_image_filename": "default.jpg",
70
+ "answers": {
71
+ "q_hobby": "Building PCs, board games, sci-fi novels.",
72
+ "q_looking_for": "A partner-in-crime for game nights and tech talks.",
73
+ "q_vibe": "Geeky and friendly.",
74
+ "q_gender_preference": "All"
75
+ }
76
+ },
77
+ "user_1p6vx64b": {
78
+ "profile_id": "user_1p6vx64b",
79
+ "auth_id": "1a824149-5ddc-4489-8856-0c774042b538",
80
+ "created_at": "2025-06-06T07:00:38.209630+00:00",
81
+ "updated_at": "2025-06-06T07:04:31.734665+00:00",
82
+ "name": "Chris",
83
+ "gender": "Man",
84
+ "profile_summary": "I am a fun guy.",
85
+ "profile_image_filename": "default.jpg",
86
+ "answers": {
87
+ "q_hobby": "I like to play video games.",
88
+ "q_looking_for": "Someone to play video games with.",
89
+ "q_vibe": "Homebody."
90
+ }
91
+ },
92
+ "user_aki9die1": {
93
+ "profile_id": "user_aki9die1",
94
+ "auth_id": "ef896a9c-01dd-4113-b996-820fd4919c17",
95
+ "created_at": "2025-06-06T08:08:36.438861+00:00",
96
+ "updated_at": "2025-06-06T08:27:00.737853+00:00",
97
+ "name": "Bob",
98
+ "gender": "Man",
99
+ "profile_summary": "Bob is a builder",
100
+ "profile_image_filename": "default.jpg",
101
+ "answers": {
102
+ "q_hobby": "Building things.",
103
+ "q_looking_for": "Someone who also likes to build things.",
104
+ "q_vibe": "Intellectual",
105
+ "q_gender_preference": "Women"
106
+ }
107
+ },
108
+ "user_7d5earg8": {
109
+ "profile_id": "user_7d5earg8",
110
+ "auth_id": "da3c732e-fb8e-4148-94e3-7e645dcc4043",
111
+ "created_at": "2025-06-06T09:14:22.028571+00:00",
112
+ "updated_at": "2025-06-06T09:18:42.887759+00:00",
113
+ "name": "Daniel",
114
+ "gender": "Man",
115
+ "profile_summary": "I'm Daniel the developer.",
116
+ "profile_image_filename": "default.jpg",
117
+ "answers": {
118
+ "q_gender_preference": "Women",
119
+ "q_hobby": "Development.",
120
+ "q_looking_for": "A woman interested in developing with me.",
121
+ "q_vibe": "Developer."
122
+ }
123
+ },
124
+ "user_lmrkaaim": {
125
+ "profile_id": "user_lmrkaaim",
126
+ "auth_id": "387d3ca8-e658-4bd2-8002-8669145707e0",
127
+ "created_at": "2025-06-07T10:31:50.273501+00:00",
128
+ "updated_at": "2025-06-08T08:54:55.660737+00:00",
129
+ "name": "Karin",
130
+ "gender": "Woman",
131
+ "profile_summary": "I'm a homebody, very intellectual but I like to dance",
132
+ "profile_image_filename": "user_lmrkaaim.png",
133
+ "answers": {
134
+ "q_gender_preference": "Men",
135
+ "q_hobby": "dancing, reading, and AI research",
136
+ "q_looking_for": "someone with similar interests but also someone who is totally jacked while not being a total jerk or meathead",
137
+ "q_vibe": "quiet conservative but open minded and fiesty"
138
+ }
139
+ },
140
+ "user_vqisd3q2": {
141
+ "profile_id": "user_vqisd3q2",
142
+ "auth_id": "7cd0f843-6b01-4157-b68b-5e980089e264",
143
+ "created_at": "2025-06-08T06:26:33.634037+00:00",
144
+ "updated_at": "2025-06-08T06:26:33.634037+00:00",
145
+ "name": "",
146
+ "gender": "",
147
+ "profile_summary": "",
148
+ "profile_image_filename": "default.jpg",
149
+ "answers": {}
150
+ }
151
+ }
data/questionnaire.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "MCP Matchmaker Profile Questionnaire",
3
+ "questions": [
4
+ { "id": "q_name", "text": "What is your name?", "type": "text", "purpose": "metadata", "maps_to_field": "name" },
5
+ { "id": "q_gender", "text": "What is your gender? Please choose one from: Man, Woman, Non-binary", "type": "text", "purpose": "metadata", "maps_to_field": "gender" },
6
+ { "id": "q_gender_preference", "text": "What gender are you interested in matching with? Please choose one from: Men, Women, All", "type": "text", "purpose": "matchmaking" },
7
+ { "id": "q_profile_summary", "text": "Write a brief introduction for your profile (1-2 sentences):", "type": "text", "purpose": "metadata", "maps_to_field": "profile_summary" },
8
+ { "id": "q_hobby", "text": "What are your main hobbies or interests?", "type": "text", "purpose": "matchmaking" },
9
+ { "id": "q_looking_for", "text": "What are you looking for in a match?", "type": "long_text", "purpose": "matchmaking" },
10
+ { "id": "q_vibe", "text": "Describe your general vibe (e.g., adventurous, homebody, intellectual)?", "type": "text", "purpose": "matchmaking" }
11
+ ]
12
+ }
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio[mcp]