Meet Patel commited on
Commit
def69a7
·
1 Parent(s): 71c0fd0

switch mcp server stdio to sse.

Browse files
Files changed (9) hide show
  1. .gitignore +3 -0
  2. app.py +13 -40
  3. client.py +100 -59
  4. docs/API.md +178 -0
  5. main.py +65 -486
  6. pyproject.toml +8 -1
  7. requirements.txt +19 -0
  8. run.py +45 -14
  9. uv.lock +0 -0
.gitignore CHANGED
@@ -8,3 +8,6 @@ wheels/
8
 
9
  # Virtual environments
10
  .venv
 
 
 
 
8
 
9
  # Virtual environments
10
  .venv
11
+
12
+ # Documentation
13
+ ARCHITECTURE.md
app.py CHANGED
@@ -9,6 +9,7 @@ import base64
9
  from io import BytesIO
10
  from PIL import Image
11
  from datetime import datetime
 
12
 
13
  # Import MCP client to communicate with the MCP server
14
  from client import client
@@ -24,10 +25,6 @@ def image_to_base64(img):
24
  img_str = base64.b64encode(buffered.getvalue()).decode()
25
  return img_str
26
 
27
- def format_json(data):
28
- """Format JSON data for display"""
29
- return json.dumps(data, indent=2)
30
-
31
  # Create Gradio interface
32
  with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
33
  gr.Markdown("# 📚 TutorX Educational AI Platform")
@@ -57,7 +54,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
57
  with gr.Column():
58
  assessment_output = gr.JSON(label="Skill Assessment")
59
  assess_btn.click(
60
- fn=lambda concept: client.assess_skill(student_id, concept),
61
  inputs=[concept_id_input],
62
  outputs=[assessment_output]
63
  )
@@ -67,7 +64,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
67
  concept_graph_output = gr.JSON(label="Concept Graph")
68
 
69
  concept_graph_btn.click(
70
- fn=lambda: client.get_concept_graph(),
71
  inputs=[],
72
  outputs=[concept_graph_output]
73
  )
@@ -87,7 +84,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
87
  quiz_output = gr.JSON(label="Generated Quiz")
88
 
89
  gen_quiz_btn.click(
90
- fn=lambda concepts, diff: client.generate_quiz(concepts, diff),
91
  inputs=[concepts_input, diff_input],
92
  outputs=[quiz_output]
93
  )
@@ -106,7 +103,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
106
  with gr.Column():
107
  lesson_output = gr.JSON(label="Lesson Plan")
108
  gen_lesson_btn.click(
109
- fn=lambda topic, grade, duration: client.generate_lesson(topic, grade, duration),
110
  inputs=[topic_input, grade_input, duration_input],
111
  outputs=[lesson_output]
112
  )
@@ -126,7 +123,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
126
  standards_output = gr.JSON(label="Curriculum Standards")
127
 
128
  standards_btn.click(
129
- fn=lambda country: client.get_curriculum_standards(country),
130
  inputs=[country_input],
131
  outputs=[standards_output]
132
  )
@@ -143,7 +140,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
143
  with gr.Column():
144
  text_output = gr.JSON(label="Response")
145
  text_btn.click(
146
- fn=lambda query: client.text_interaction(query, student_id),
147
  inputs=[text_input],
148
  outputs=[text_output]
149
  )
@@ -158,9 +155,8 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
158
  with gr.Column():
159
  drawing_output = gr.JSON(label="Recognition Results")
160
 
161
- # Convert drawing to base64 then process
162
  drawing_btn.click(
163
- fn=lambda img: client.handwriting_recognition(image_to_base64(img), student_id),
164
  inputs=[drawing_input],
165
  outputs=[drawing_output]
166
  )
@@ -172,7 +168,7 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
172
  timeframe = gr.Slider(minimum=7, maximum=90, value=30, step=1, label="Timeframe (days)")
173
  analytics_output = gr.JSON(label="Performance Analytics")
174
  analytics_btn.click(
175
- fn=lambda days: client.get_student_analytics(student_id, days),
176
  inputs=[timeframe],
177
  outputs=[analytics_output]
178
  )
@@ -188,33 +184,10 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
188
  error_output = gr.JSON(label="Error Pattern Analysis")
189
 
190
  error_btn.click(
191
- fn=lambda concept: client.analyze_error_patterns(student_id, concept),
192
  inputs=[error_concept],
193
  outputs=[error_output]
194
  )
195
-
196
- # Tab 5: Assessment Tools
197
- with gr.Tab("Assessment Tools"):
198
- gr.Markdown("## Create Assessment")
199
-
200
- with gr.Row():
201
- with gr.Column():
202
- assess_concepts = gr.CheckboxGroup(
203
- choices=["math_algebra_basics", "math_algebra_linear_equations", "math_algebra_quadratic_equations"],
204
- label="Select Concepts",
205
- value=["math_algebra_linear_equations"]
206
- )
207
- assess_questions = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Number of Questions")
208
- assess_diff = gr.Slider(minimum=1, maximum=5, value=3, step=1, label="Difficulty")
209
- create_assess_btn = gr.Button("Create Assessment")
210
-
211
- with gr.Column():
212
- assessment_output = gr.JSON(label="Generated Assessment")
213
- create_assess_btn.click(
214
- fn=lambda concepts, num, diff: client.create_assessment(concepts, num, diff),
215
- inputs=[assess_concepts, assess_questions, assess_diff],
216
- outputs=[assessment_output]
217
- )
218
 
219
  gr.Markdown("## Plagiarism Detection")
220
 
@@ -236,11 +209,11 @@ with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
236
  plagiarism_output = gr.JSON(label="Originality Report")
237
 
238
  plagiarism_btn.click(
239
- fn=lambda sub, ref: client.check_submission_originality(sub, [ref]),
240
  inputs=[submission_input, reference_input],
241
  outputs=[plagiarism_output]
242
  )
243
 
244
- # Launch the app
245
  if __name__ == "__main__":
246
- demo.launch()
 
9
  from io import BytesIO
10
  from PIL import Image
11
  from datetime import datetime
12
+ import asyncio
13
 
14
  # Import MCP client to communicate with the MCP server
15
  from client import client
 
25
  img_str = base64.b64encode(buffered.getvalue()).decode()
26
  return img_str
27
 
 
 
 
 
28
  # Create Gradio interface
29
  with gr.Blocks(title="TutorX Educational AI", theme=gr.themes.Soft()) as demo:
30
  gr.Markdown("# 📚 TutorX Educational AI Platform")
 
54
  with gr.Column():
55
  assessment_output = gr.JSON(label="Skill Assessment")
56
  assess_btn.click(
57
+ fn=lambda x: asyncio.run(client.assess_skill("student_12345", x)),
58
  inputs=[concept_id_input],
59
  outputs=[assessment_output]
60
  )
 
64
  concept_graph_output = gr.JSON(label="Concept Graph")
65
 
66
  concept_graph_btn.click(
67
+ fn=lambda: asyncio.run(client.get_concept_graph()),
68
  inputs=[],
69
  outputs=[concept_graph_output]
70
  )
 
84
  quiz_output = gr.JSON(label="Generated Quiz")
85
 
86
  gen_quiz_btn.click(
87
+ fn=lambda x, y: asyncio.run(client.generate_quiz(x, y)),
88
  inputs=[concepts_input, diff_input],
89
  outputs=[quiz_output]
90
  )
 
103
  with gr.Column():
104
  lesson_output = gr.JSON(label="Lesson Plan")
105
  gen_lesson_btn.click(
106
+ fn=lambda x, y, z: asyncio.run(client.generate_lesson(x, y, z)),
107
  inputs=[topic_input, grade_input, duration_input],
108
  outputs=[lesson_output]
109
  )
 
123
  standards_output = gr.JSON(label="Curriculum Standards")
124
 
125
  standards_btn.click(
126
+ fn=lambda x: asyncio.run(client.get_curriculum_standards(x)),
127
  inputs=[country_input],
128
  outputs=[standards_output]
129
  )
 
140
  with gr.Column():
141
  text_output = gr.JSON(label="Response")
142
  text_btn.click(
143
+ fn=lambda x: asyncio.run(client.text_interaction(x, "student_12345")),
144
  inputs=[text_input],
145
  outputs=[text_output]
146
  )
 
155
  with gr.Column():
156
  drawing_output = gr.JSON(label="Recognition Results")
157
 
 
158
  drawing_btn.click(
159
+ fn=lambda x: asyncio.run(client.handwriting_recognition(image_to_base64(x), "student_12345")),
160
  inputs=[drawing_input],
161
  outputs=[drawing_output]
162
  )
 
168
  timeframe = gr.Slider(minimum=7, maximum=90, value=30, step=1, label="Timeframe (days)")
169
  analytics_output = gr.JSON(label="Performance Analytics")
170
  analytics_btn.click(
171
+ fn=lambda x: asyncio.run(client.get_student_analytics("student_12345", x)),
172
  inputs=[timeframe],
173
  outputs=[analytics_output]
174
  )
 
184
  error_output = gr.JSON(label="Error Pattern Analysis")
185
 
186
  error_btn.click(
187
+ fn=lambda x: asyncio.run(client.analyze_error_patterns("student_12345", x)),
188
  inputs=[error_concept],
189
  outputs=[error_output]
190
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  gr.Markdown("## Plagiarism Detection")
193
 
 
209
  plagiarism_output = gr.JSON(label="Originality Report")
210
 
211
  plagiarism_btn.click(
212
+ fn=lambda x, y: asyncio.run(client.check_submission_originality(x, [y])),
213
  inputs=[submission_input, reference_input],
214
  outputs=[plagiarism_output]
215
  )
216
 
217
+ # Launch the interface
218
  if __name__ == "__main__":
219
+ demo.launch(server_name="0.0.0.0", server_port=7860)
client.py CHANGED
@@ -5,21 +5,36 @@ for use by the Gradio interface.
5
  """
6
 
7
  import json
8
- import requests
 
9
  from typing import Dict, Any, List, Optional
10
  import base64
11
  from datetime import datetime
 
12
 
13
- # Default MCP server URL
14
- MCP_SERVER_URL = "http://localhost:8000"
 
 
15
 
16
  class TutorXClient:
17
  """Client for interacting with the TutorX MCP server"""
18
 
19
- def __init__(self, server_url=MCP_SERVER_URL):
20
  self.server_url = server_url
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- def _call_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
23
  """
24
  Call an MCP tool on the server
25
 
@@ -30,21 +45,22 @@ class TutorXClient:
30
  Returns:
31
  Tool response
32
  """
 
33
  try:
34
- response = requests.post(
35
- f"{self.server_url}/tools/{tool_name}",
36
  json=params,
37
- headers={"Content-Type": "application/json"}
38
- )
39
- response.raise_for_status()
40
- return response.json()
41
- except requests.RequestException as e:
42
  return {
43
  "error": f"Failed to call tool: {str(e)}",
44
  "timestamp": datetime.now().isoformat()
45
  }
46
 
47
- def _get_resource(self, resource_uri: str) -> Dict[str, Any]:
48
  """
49
  Get an MCP resource from the server
50
 
@@ -54,72 +70,90 @@ class TutorXClient:
54
  Returns:
55
  Resource data
56
  """
 
57
  try:
58
- response = requests.get(
59
- f"{self.server_url}/resources?uri={resource_uri}",
60
- headers={"Accept": "application/json"}
61
- )
62
- response.raise_for_status()
63
- return response.json()
64
- except requests.RequestException as e:
65
  return {
66
  "error": f"Failed to get resource: {str(e)}",
67
  "timestamp": datetime.now().isoformat()
68
  }
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  # ------------ Core Features ------------
71
 
72
- def assess_skill(self, student_id: str, concept_id: str) -> Dict[str, Any]:
73
  """Assess student's skill level on a specific concept"""
74
- return self._call_tool("assess_skill", {
75
  "student_id": student_id,
76
  "concept_id": concept_id
77
  })
78
 
79
- def get_concept_graph(self) -> Dict[str, Any]:
80
  """Get the full knowledge concept graph"""
81
- return self._get_resource("concept-graph://")
82
 
83
- def get_learning_path(self, student_id: str) -> Dict[str, Any]:
84
  """Get personalized learning path for a student"""
85
- return self._get_resource(f"learning-path://{student_id}")
86
 
87
- def generate_quiz(self, concept_ids: List[str], difficulty: int = 2) -> Dict[str, Any]:
88
  """Generate a quiz based on specified concepts and difficulty"""
89
- return self._call_tool("generate_quiz", {
90
  "concept_ids": concept_ids,
91
  "difficulty": difficulty
92
  })
93
 
94
- def analyze_error_patterns(self, student_id: str, concept_id: str) -> Dict[str, Any]:
95
  """Analyze common error patterns for a student on a specific concept"""
96
- return self._call_tool("analyze_error_patterns", {
97
  "student_id": student_id,
98
  "concept_id": concept_id
99
  })
100
 
101
  # ------------ Advanced Features ------------
102
 
103
- def analyze_cognitive_state(self, eeg_data: Dict[str, Any]) -> Dict[str, Any]:
104
  """Analyze EEG data to determine cognitive state"""
105
- return self._call_tool("analyze_cognitive_state", {
106
  "eeg_data": eeg_data
107
  })
108
 
109
- def get_curriculum_standards(self, country_code: str) -> Dict[str, Any]:
110
  """Get curriculum standards for a specific country"""
111
- return self._get_resource(f"curriculum-standards://{country_code}")
112
 
113
- def align_content_to_standard(self, content_id: str, standard_id: str) -> Dict[str, Any]:
114
  """Align educational content to a specific curriculum standard"""
115
- return self._call_tool("align_content_to_standard", {
116
  "content_id": content_id,
117
  "standard_id": standard_id
118
  })
119
 
120
- def generate_lesson(self, topic: str, grade_level: int, duration_minutes: int = 45) -> Dict[str, Any]:
121
  """Generate a complete lesson plan on a topic"""
122
- return self._call_tool("generate_lesson", {
123
  "topic": topic,
124
  "grade_level": grade_level,
125
  "duration_minutes": duration_minutes
@@ -127,76 +161,83 @@ class TutorXClient:
127
 
128
  # ------------ User Experience ------------
129
 
130
- def get_student_dashboard(self, student_id: str) -> Dict[str, Any]:
131
  """Get dashboard data for a specific student"""
132
- return self._get_resource(f"student-dashboard://{student_id}")
133
 
134
- def get_accessibility_settings(self, student_id: str) -> Dict[str, Any]:
135
  """Get accessibility settings for a student"""
136
- return self._call_tool("get_accessibility_settings", {
137
  "student_id": student_id
138
  })
139
 
140
- def update_accessibility_settings(self, student_id: str, settings: Dict[str, Any]) -> Dict[str, Any]:
141
  """Update accessibility settings for a student"""
142
- return self._call_tool("update_accessibility_settings", {
143
  "student_id": student_id,
144
  "settings": settings
145
  })
146
 
147
  # ------------ Multi-Modal Interaction ------------
148
 
149
- def text_interaction(self, query: str, student_id: str) -> Dict[str, Any]:
150
  """Process a text query from the student"""
151
- return self._call_tool("text_interaction", {
152
  "query": query,
153
  "student_id": student_id
154
  })
155
 
156
- def voice_interaction(self, audio_data_base64: str, student_id: str) -> Dict[str, Any]:
157
  """Process voice input from the student"""
158
- return self._call_tool("voice_interaction", {
159
  "audio_data_base64": audio_data_base64,
160
  "student_id": student_id
161
  })
162
 
163
- def handwriting_recognition(self, image_data_base64: str, student_id: str) -> Dict[str, Any]:
164
  """Process handwritten input from the student"""
165
- return self._call_tool("handwriting_recognition", {
166
  "image_data_base64": image_data_base64,
167
  "student_id": student_id
168
  })
169
 
170
  # ------------ Assessment ------------
171
 
172
- def create_assessment(self, concept_ids: List[str], num_questions: int, difficulty: int = 3) -> Dict[str, Any]:
173
  """Create a complete assessment for given concepts"""
174
- return self._call_tool("create_assessment", {
175
  "concept_ids": concept_ids,
176
  "num_questions": num_questions,
177
  "difficulty": difficulty
178
  })
179
 
180
- def grade_assessment(self, assessment_id: str, student_answers: Dict[str, str], questions: List[Dict[str, Any]]) -> Dict[str, Any]:
181
  """Grade a completed assessment"""
182
- return self._call_tool("grade_assessment", {
183
  "assessment_id": assessment_id,
184
  "student_answers": student_answers,
185
  "questions": questions
186
  })
187
 
188
- def get_student_analytics(self, student_id: str, timeframe_days: int = 30) -> Dict[str, Any]:
189
  """Get comprehensive analytics for a student"""
190
- return self._call_tool("get_student_analytics", {
191
  "student_id": student_id,
192
  "timeframe_days": timeframe_days
193
- })
194
- def check_submission_originality(self, submission: str, reference_sources: List[str]) -> Dict[str, Any]:
 
195
  """Check student submission for potential plagiarism"""
196
- return self._call_tool("check_submission_originality", {
197
  "submission": submission,
198
  "reference_sources": reference_sources
199
  })
 
 
 
 
 
 
200
 
201
  # Create a default client instance for easy import
202
  client = TutorXClient()
 
5
  """
6
 
7
  import json
8
+ import aiohttp
9
+ import asyncio
10
  from typing import Dict, Any, List, Optional
11
  import base64
12
  from datetime import datetime
13
+ import os
14
 
15
+ # Get server configuration from environment variables with defaults
16
+ DEFAULT_HOST = os.getenv("MCP_HOST", "127.0.0.1")
17
+ DEFAULT_PORT = int(os.getenv("MCP_PORT", "8000"))
18
+ DEFAULT_SERVER_URL = f"http://{DEFAULT_HOST}:{DEFAULT_PORT}"
19
 
20
  class TutorXClient:
21
  """Client for interacting with the TutorX MCP server"""
22
 
23
+ def __init__(self, server_url=DEFAULT_SERVER_URL):
24
  self.server_url = server_url
25
+ self.session = None
26
+
27
+ async def _ensure_session(self):
28
+ """Ensure aiohttp session exists"""
29
+ if self.session is None:
30
+ self.session = aiohttp.ClientSession(
31
+ headers={
32
+ "Content-Type": "application/json",
33
+ "Accept": "application/json"
34
+ }
35
+ )
36
 
37
+ async def _call_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
38
  """
39
  Call an MCP tool on the server
40
 
 
45
  Returns:
46
  Tool response
47
  """
48
+ await self._ensure_session()
49
  try:
50
+ async with self.session.post(
51
+ f"{self.server_url}/mcp/tools/{tool_name}",
52
  json=params,
53
+ timeout=30
54
+ ) as response:
55
+ response.raise_for_status()
56
+ return await response.json()
57
+ except Exception as e:
58
  return {
59
  "error": f"Failed to call tool: {str(e)}",
60
  "timestamp": datetime.now().isoformat()
61
  }
62
 
63
+ async def _get_resource(self, resource_uri: str) -> Dict[str, Any]:
64
  """
65
  Get an MCP resource from the server
66
 
 
70
  Returns:
71
  Resource data
72
  """
73
+ await self._ensure_session()
74
  try:
75
+ async with self.session.get(
76
+ f"{self.server_url}/mcp/resources?uri={resource_uri}",
77
+ timeout=30
78
+ ) as response:
79
+ response.raise_for_status()
80
+ return await response.json()
81
+ except Exception as e:
82
  return {
83
  "error": f"Failed to get resource: {str(e)}",
84
  "timestamp": datetime.now().isoformat()
85
  }
86
 
87
+ async def check_server_connection(self) -> bool:
88
+ """
89
+ Check if the server is accessible
90
+
91
+ Returns:
92
+ bool: True if server is accessible, False otherwise
93
+ """
94
+ await self._ensure_session()
95
+ try:
96
+ async with self.session.get(
97
+ f"{self.server_url}/health",
98
+ timeout=5
99
+ ) as response:
100
+ return response.status == 200
101
+ except:
102
+ return False
103
+
104
  # ------------ Core Features ------------
105
 
106
+ async def assess_skill(self, student_id: str, concept_id: str) -> Dict[str, Any]:
107
  """Assess student's skill level on a specific concept"""
108
+ return await self._call_tool("assess_skill", {
109
  "student_id": student_id,
110
  "concept_id": concept_id
111
  })
112
 
113
+ async def get_concept_graph(self) -> Dict[str, Any]:
114
  """Get the full knowledge concept graph"""
115
+ return await self._get_resource("concept-graph://")
116
 
117
+ async def get_learning_path(self, student_id: str) -> Dict[str, Any]:
118
  """Get personalized learning path for a student"""
119
+ return await self._get_resource(f"learning-path://{student_id}")
120
 
121
+ async def generate_quiz(self, concept_ids: List[str], difficulty: int = 2) -> Dict[str, Any]:
122
  """Generate a quiz based on specified concepts and difficulty"""
123
+ return await self._call_tool("generate_quiz", {
124
  "concept_ids": concept_ids,
125
  "difficulty": difficulty
126
  })
127
 
128
+ async def analyze_error_patterns(self, student_id: str, concept_id: str) -> Dict[str, Any]:
129
  """Analyze common error patterns for a student on a specific concept"""
130
+ return await self._call_tool("analyze_error_patterns", {
131
  "student_id": student_id,
132
  "concept_id": concept_id
133
  })
134
 
135
  # ------------ Advanced Features ------------
136
 
137
+ async def analyze_cognitive_state(self, eeg_data: Dict[str, Any]) -> Dict[str, Any]:
138
  """Analyze EEG data to determine cognitive state"""
139
+ return await self._call_tool("analyze_cognitive_state", {
140
  "eeg_data": eeg_data
141
  })
142
 
143
+ async def get_curriculum_standards(self, country_code: str) -> Dict[str, Any]:
144
  """Get curriculum standards for a specific country"""
145
+ return await self._get_resource(f"curriculum-standards://{country_code}")
146
 
147
+ async def align_content_to_standard(self, content_id: str, standard_id: str) -> Dict[str, Any]:
148
  """Align educational content to a specific curriculum standard"""
149
+ return await self._call_tool("align_content_to_standard", {
150
  "content_id": content_id,
151
  "standard_id": standard_id
152
  })
153
 
154
+ async def generate_lesson(self, topic: str, grade_level: int, duration_minutes: int = 45) -> Dict[str, Any]:
155
  """Generate a complete lesson plan on a topic"""
156
+ return await self._call_tool("generate_lesson", {
157
  "topic": topic,
158
  "grade_level": grade_level,
159
  "duration_minutes": duration_minutes
 
161
 
162
  # ------------ User Experience ------------
163
 
164
+ async def get_student_dashboard(self, student_id: str) -> Dict[str, Any]:
165
  """Get dashboard data for a specific student"""
166
+ return await self._get_resource(f"student-dashboard://{student_id}")
167
 
168
+ async def get_accessibility_settings(self, student_id: str) -> Dict[str, Any]:
169
  """Get accessibility settings for a student"""
170
+ return await self._call_tool("get_accessibility_settings", {
171
  "student_id": student_id
172
  })
173
 
174
+ async def update_accessibility_settings(self, student_id: str, settings: Dict[str, Any]) -> Dict[str, Any]:
175
  """Update accessibility settings for a student"""
176
+ return await self._call_tool("update_accessibility_settings", {
177
  "student_id": student_id,
178
  "settings": settings
179
  })
180
 
181
  # ------------ Multi-Modal Interaction ------------
182
 
183
+ async def text_interaction(self, query: str, student_id: str) -> Dict[str, Any]:
184
  """Process a text query from the student"""
185
+ return await self._call_tool("text_interaction", {
186
  "query": query,
187
  "student_id": student_id
188
  })
189
 
190
+ async def voice_interaction(self, audio_data_base64: str, student_id: str) -> Dict[str, Any]:
191
  """Process voice input from the student"""
192
+ return await self._call_tool("voice_interaction", {
193
  "audio_data_base64": audio_data_base64,
194
  "student_id": student_id
195
  })
196
 
197
+ async def handwriting_recognition(self, image_data_base64: str, student_id: str) -> Dict[str, Any]:
198
  """Process handwritten input from the student"""
199
+ return await self._call_tool("handwriting_recognition", {
200
  "image_data_base64": image_data_base64,
201
  "student_id": student_id
202
  })
203
 
204
  # ------------ Assessment ------------
205
 
206
+ async def create_assessment(self, concept_ids: List[str], num_questions: int, difficulty: int = 3) -> Dict[str, Any]:
207
  """Create a complete assessment for given concepts"""
208
+ return await self._call_tool("create_assessment", {
209
  "concept_ids": concept_ids,
210
  "num_questions": num_questions,
211
  "difficulty": difficulty
212
  })
213
 
214
+ async def grade_assessment(self, assessment_id: str, student_answers: Dict[str, str], questions: List[Dict[str, Any]]) -> Dict[str, Any]:
215
  """Grade a completed assessment"""
216
+ return await self._call_tool("grade_assessment", {
217
  "assessment_id": assessment_id,
218
  "student_answers": student_answers,
219
  "questions": questions
220
  })
221
 
222
+ async def get_student_analytics(self, student_id: str, timeframe_days: int = 30) -> Dict[str, Any]:
223
  """Get comprehensive analytics for a student"""
224
+ return await self._call_tool("get_student_analytics", {
225
  "student_id": student_id,
226
  "timeframe_days": timeframe_days
227
+ })
228
+
229
+ async def check_submission_originality(self, submission: str, reference_sources: List[str]) -> Dict[str, Any]:
230
  """Check student submission for potential plagiarism"""
231
+ return await self._call_tool("check_submission_originality", {
232
  "submission": submission,
233
  "reference_sources": reference_sources
234
  })
235
+
236
+ async def close(self):
237
+ """Close the aiohttp session"""
238
+ if self.session:
239
+ await self.session.close()
240
+ self.session = None
241
 
242
  # Create a default client instance for easy import
243
  client = TutorXClient()
docs/API.md ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TutorX MCP API Documentation
2
+
3
+ ## Overview
4
+
5
+ The TutorX MCP API provides a comprehensive set of endpoints for educational tools and resources. The API follows RESTful principles and uses JSON for request/response bodies.
6
+
7
+ ## Base URL
8
+
9
+ ```
10
+ http://127.0.0.1:8000
11
+ ```
12
+
13
+ ## Authentication
14
+
15
+ Currently, the API does not require authentication. This will be implemented in future versions.
16
+
17
+ ## Endpoints
18
+
19
+ ### Health Check
20
+
21
+ ```http
22
+ GET /health
23
+ ```
24
+
25
+ Returns the health status of the server.
26
+
27
+ **Response**
28
+ ```json
29
+ {
30
+ "status": "healthy",
31
+ "timestamp": "2024-03-14T12:00:00.000Z",
32
+ "server": "TutorX MCP",
33
+ "version": "1.0.0"
34
+ }
35
+ ```
36
+
37
+ ### Core Features
38
+
39
+ #### Assess Skill
40
+
41
+ ```http
42
+ POST /mcp/tools/assess_skill
43
+ ```
44
+
45
+ Assesses a student's skill level on a specific concept.
46
+
47
+ **Request Body**
48
+ ```json
49
+ {
50
+ "student_id": "string",
51
+ "concept_id": "string"
52
+ }
53
+ ```
54
+
55
+ **Response**
56
+ ```json
57
+ {
58
+ "student_id": "string",
59
+ "concept_id": "string",
60
+ "skill_level": 0.75,
61
+ "confidence": 0.85,
62
+ "recommendations": [
63
+ "Practice more complex problems",
64
+ "Review related concept: algebra_linear_equations"
65
+ ],
66
+ "timestamp": "2024-03-14T12:00:00.000Z"
67
+ }
68
+ ```
69
+
70
+ #### Get Concept Graph
71
+
72
+ ```http
73
+ GET /mcp/resources/concept-graph://
74
+ ```
75
+
76
+ Returns the full knowledge concept graph.
77
+
78
+ **Response**
79
+ ```json
80
+ {
81
+ "nodes": [
82
+ {
83
+ "id": "math_algebra_basics",
84
+ "name": "Algebra Basics",
85
+ "difficulty": 1
86
+ }
87
+ ],
88
+ "edges": [
89
+ {
90
+ "from": "math_algebra_basics",
91
+ "to": "math_algebra_linear_equations",
92
+ "weight": 1.0
93
+ }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ #### Get Learning Path
99
+
100
+ ```http
101
+ GET /mcp/resources/learning-path://{student_id}
102
+ ```
103
+
104
+ Returns a personalized learning path for a student.
105
+
106
+ **Response**
107
+ ```json
108
+ {
109
+ "student_id": "string",
110
+ "current_concepts": ["math_algebra_linear_equations"],
111
+ "recommended_next": ["math_algebra_quadratic_equations"],
112
+ "mastered": ["math_algebra_basics"],
113
+ "estimated_completion_time": "2 weeks"
114
+ }
115
+ ```
116
+
117
+ #### Generate Quiz
118
+
119
+ ```http
120
+ POST /mcp/tools/generate_quiz
121
+ ```
122
+
123
+ Generates a quiz based on specified concepts and difficulty.
124
+
125
+ **Request Body**
126
+ ```json
127
+ {
128
+ "concept_ids": ["string"],
129
+ "difficulty": 2
130
+ }
131
+ ```
132
+
133
+ **Response**
134
+ ```json
135
+ {
136
+ "quiz_id": "string",
137
+ "concept_ids": ["string"],
138
+ "difficulty": 2,
139
+ "questions": [
140
+ {
141
+ "id": "string",
142
+ "text": "string",
143
+ "type": "string",
144
+ "answer": "string",
145
+ "solution_steps": ["string"]
146
+ }
147
+ ]
148
+ }
149
+ ```
150
+
151
+ ## Error Responses
152
+
153
+ The API uses standard HTTP status codes and returns error details in the response body:
154
+
155
+ ```json
156
+ {
157
+ "error": "Error message",
158
+ "timestamp": "2024-03-14T12:00:00.000Z"
159
+ }
160
+ ```
161
+
162
+ Common status codes:
163
+ - 200: Success
164
+ - 400: Bad Request
165
+ - 404: Not Found
166
+ - 500: Internal Server Error
167
+
168
+ ## Rate Limiting
169
+
170
+ Currently, there are no rate limits implemented. This will be added in future versions.
171
+
172
+ ## Versioning
173
+
174
+ The API version is included in the response headers and health check endpoint. Future versions will support versioning through the URL path.
175
+
176
+ ## Support
177
+
178
+ For support or to report issues, please contact the development team or create an issue in the project repository.
main.py CHANGED
@@ -2,8 +2,14 @@
2
  from mcp.server.fastmcp import FastMCP
3
  import json
4
  import os
 
5
  from typing import List, Dict, Any, Optional
6
  from datetime import datetime
 
 
 
 
 
7
 
8
  # Import utility functions
9
  from utils.multimodal import (
@@ -19,14 +25,29 @@ from utils.assessment import (
19
  detect_plagiarism
20
  )
21
 
22
- # Create the TutorX MCP server
23
- mcp = FastMCP("TutorX")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  # ------------------ Core Features ------------------
26
 
27
  # Adaptive Learning Engine
28
  @mcp.tool()
29
- def assess_skill(student_id: str, concept_id: str) -> Dict[str, Any]:
30
  """
31
  Assess student's skill level on a specific concept
32
 
@@ -37,21 +58,27 @@ def assess_skill(student_id: str, concept_id: str) -> Dict[str, Any]:
37
  Returns:
38
  Dictionary containing skill level and recommendations
39
  """
40
- # Simulated skill assessment
41
- return {
42
- "student_id": student_id,
43
- "concept_id": concept_id,
44
- "skill_level": 0.75,
45
- "confidence": 0.85,
46
- "recommendations": [
47
- "Practice more complex problems",
48
- "Review related concept: algebra_linear_equations"
49
- ],
50
- "timestamp": datetime.now().isoformat()
51
- }
 
 
 
 
 
 
52
 
53
  @mcp.resource("concept-graph://")
54
- def get_concept_graph() -> Dict[str, Any]:
55
  """Get the full knowledge concept graph"""
56
  return {
57
  "nodes": [
@@ -66,7 +93,7 @@ def get_concept_graph() -> Dict[str, Any]:
66
  }
67
 
68
  @mcp.resource("learning-path://{student_id}")
69
- def get_learning_path(student_id: str) -> Dict[str, Any]:
70
  """Get personalized learning path for a student"""
71
  return {
72
  "student_id": student_id,
@@ -78,7 +105,7 @@ def get_learning_path(student_id: str) -> Dict[str, Any]:
78
 
79
  # Assessment Suite
80
  @mcp.tool()
81
- def generate_quiz(concept_ids: List[str], difficulty: int = 2) -> Dict[str, Any]:
82
  """
83
  Generate a quiz based on specified concepts and difficulty
84
 
@@ -109,476 +136,28 @@ def generate_quiz(concept_ids: List[str], difficulty: int = 2) -> Dict[str, Any]
109
  ]
110
  }
111
 
112
- # Feedback System
113
- @mcp.tool()
114
- def analyze_error_patterns(student_id: str, concept_id: str) -> Dict[str, Any]:
115
- """
116
- Analyze common error patterns for a student on a specific concept
117
-
118
- Args:
119
- student_id: The student's unique identifier
120
- concept_id: The concept to analyze
121
-
122
- Returns:
123
- Error pattern analysis
124
- """
125
- return {
126
- "student_id": student_id,
127
- "concept_id": concept_id,
128
- "common_errors": [
129
- {
130
- "type": "sign_error",
131
- "frequency": 0.65,
132
- "example": "2x - 3 = 5 → 2x = 5 - 3 → 2x = 2 → x = 1 (should be x = 4)"
133
- },
134
- {
135
- "type": "arithmetic_error",
136
- "frequency": 0.35,
137
- "example": "2x = 8 → x = 8/2 = 3 (should be x = 4)"
138
- }
139
- ],
140
- "recommendations": [
141
- "Practice more sign manipulation problems",
142
- "Review basic arithmetic operations"
143
- ]
144
- }
145
-
146
- # ------------------ Advanced Features ------------------
147
-
148
- # Neurological Engagement Monitor
149
- @mcp.tool()
150
- def analyze_cognitive_state(eeg_data: Dict[str, Any]) -> Dict[str, Any]:
151
- """
152
- Analyze EEG data to determine cognitive state
153
-
154
- Args:
155
- eeg_data: Raw or processed EEG data
156
-
157
- Returns:
158
- Analysis of cognitive state
159
- """
160
- return {
161
- "attention_level": 0.82,
162
- "cognitive_load": 0.65,
163
- "stress_level": 0.25,
164
- "recommendations": [
165
- "Student is engaged but approaching cognitive overload",
166
- "Consider simplifying next problems slightly"
167
- ],
168
- "timestamp": datetime.now().isoformat()
169
- }
170
-
171
- # Cross-Institutional Knowledge Fusion
172
- @mcp.resource("curriculum-standards://{country_code}")
173
- def get_curriculum_standards(country_code: str) -> Dict[str, Any]:
174
- """Get curriculum standards for a specific country"""
175
- standards = {
176
- "us": {
177
- "name": "Common Core State Standards",
178
- "math_standards": {
179
- "algebra_1": [
180
- "CCSS.Math.Content.HSA.CED.A.1",
181
- "CCSS.Math.Content.HSA.CED.A.2"
182
- ]
183
- }
184
- },
185
- "uk": {
186
- "name": "National Curriculum",
187
- "math_standards": {
188
- "algebra_1": [
189
- "KS3.Algebra.1",
190
- "KS3.Algebra.2"
191
- ]
192
- }
193
- }
194
- }
195
-
196
- return standards.get(country_code.lower(), {"error": "Country code not found"})
197
-
198
- @mcp.tool()
199
- def align_content_to_standard(content_id: str, standard_id: str) -> Dict[str, Any]:
200
- """
201
- Align educational content to a specific curriculum standard
202
-
203
- Args:
204
- content_id: The ID of the content to align
205
- standard_id: The curriculum standard ID
206
-
207
- Returns:
208
- Alignment information and recommendations
209
- """
210
- return {
211
- "content_id": content_id,
212
- "standard_id": standard_id,
213
- "alignment_score": 0.85,
214
- "gaps": [
215
- "Missing coverage of polynomial division",
216
- "Should include more word problems"
217
- ],
218
- "recommendations": [
219
- "Add 2-3 examples of polynomial division",
220
- "Convert 30% of problems to word problems"
221
- ]
222
- }
223
-
224
- # Automated Lesson Authoring
225
- @mcp.tool()
226
- def generate_lesson(topic: str, grade_level: int, duration_minutes: int = 45) -> Dict[str, Any]:
227
- """
228
- Generate a complete lesson plan on a topic
229
-
230
- Args:
231
- topic: The lesson topic
232
- grade_level: Target grade level (K-12)
233
- duration_minutes: Lesson duration in minutes
234
-
235
- Returns:
236
- Complete lesson plan
237
- """
238
- return {
239
- "topic": topic,
240
- "grade_level": grade_level,
241
- "duration_minutes": duration_minutes,
242
- "objectives": [
243
- "Students will be able to solve linear equations in one variable",
244
- "Students will be able to check their solutions"
245
- ],
246
- "materials": [
247
- "Whiteboard/projector",
248
- "Handouts with practice problems",
249
- "Graphing calculators (optional)"
250
- ],
251
- "activities": [
252
- {
253
- "name": "Warm-up",
254
- "duration_minutes": 5,
255
- "description": "Review of pre-algebra concepts needed for today's lesson"
256
- },
257
- {
258
- "name": "Direct Instruction",
259
- "duration_minutes": 15,
260
- "description": "Teacher demonstrates solving linear equations step by step"
261
- },
262
- {
263
- "name": "Guided Practice",
264
- "duration_minutes": 10,
265
- "description": "Students solve problems with teacher guidance"
266
- },
267
- {
268
- "name": "Independent Practice",
269
- "duration_minutes": 10,
270
- "description": "Students solve problems independently"
271
- },
272
- {
273
- "name": "Closure",
274
- "duration_minutes": 5,
275
- "description": "Review key concepts and preview next lesson"
276
- }
277
- ],
278
- "assessment": {
279
- "formative": "Teacher observation during guided and independent practice",
280
- "summative": "Exit ticket with 3 problems to solve"
281
- },
282
- "differentiation": {
283
- "struggling": "Provide equation-solving steps reference sheet",
284
- "advanced": "Offer multi-step equations with fractions and decimals"
285
- }
286
- }
287
-
288
- # ------------------ User Experience Features ------------------
289
-
290
- @mcp.resource("student-dashboard://{student_id}")
291
- def get_student_dashboard(student_id: str) -> Dict[str, Any]:
292
- """Get dashboard data for a specific student"""
293
- return {
294
- "student_id": student_id,
295
- "knowledge_map": {
296
- "mastery_percentage": 68,
297
- "concepts_mastered": 42,
298
- "concepts_in_progress": 15,
299
- "concepts_not_started": 25
300
- },
301
- "recent_activity": [
302
- {
303
- "timestamp": "2025-06-06T15:30:00Z",
304
- "activity_type": "quiz",
305
- "description": "Algebra Quiz #3",
306
- "performance": "85%"
307
- },
308
- {
309
- "timestamp": "2025-06-05T13:45:00Z",
310
- "activity_type": "lesson",
311
- "description": "Quadratic Equations Introduction",
312
- "duration_minutes": 32
313
- }
314
- ],
315
- "recommendations": [
316
- "Complete Factor Polynomials practice set",
317
- "Review Linear Systems interactive module"
318
- ]
319
- }
320
-
321
- @mcp.tool()
322
- def get_accessibility_settings(student_id: str) -> Dict[str, Any]:
323
- """
324
- Get accessibility settings for a student
325
-
326
- Args:
327
- student_id: The student's unique identifier
328
-
329
- Returns:
330
- Accessibility settings
331
- """
332
- return {
333
- "student_id": student_id,
334
- "text_to_speech_enabled": True,
335
- "font_size": "large",
336
- "high_contrast_mode": False,
337
- "screen_reader_compatible": True,
338
- "keyboard_navigation_enabled": True
339
- }
340
-
341
- @mcp.tool()
342
- def update_accessibility_settings(student_id: str, settings: Dict[str, Any]) -> Dict[str, Any]:
343
- """
344
- Update accessibility settings for a student
345
-
346
- Args:
347
- student_id: The student's unique identifier
348
- settings: Dictionary of settings to update
349
-
350
- Returns:
351
- Updated accessibility settings
352
- """
353
- # In a real implementation, this would update a database
354
- return {
355
- "student_id": student_id,
356
- "text_to_speech_enabled": settings.get("text_to_speech_enabled", True),
357
- "font_size": settings.get("font_size", "large"),
358
- "high_contrast_mode": settings.get("high_contrast_mode", False),
359
- "screen_reader_compatible": settings.get("screen_reader_compatible", True),
360
- "keyboard_navigation_enabled": settings.get("keyboard_navigation_enabled", True),
361
- "updated_at": datetime.now().isoformat()
362
- }
363
-
364
- # ------------------ Multi-Modal Interaction ------------------
365
-
366
- @mcp.tool()
367
- @mcp.tool()
368
- def text_interaction(query: str, student_id: str, session_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
369
- """
370
- Process a text query from the student
371
-
372
- Args:
373
- query: The text query from the student
374
- student_id: The student's unique identifier
375
- session_context: Optional context about the current session
376
-
377
- Returns:
378
- Processed response
379
- """
380
- # Add student information to context
381
- context = session_context or {}
382
- context["student_id"] = student_id
383
-
384
- return process_text_query(query, context)
385
-
386
- @mcp.tool()
387
- def voice_interaction(audio_data_base64: str, student_id: str) -> Dict[str, Any]:
388
- """
389
- Process voice input from the student
390
-
391
- Args:
392
- audio_data_base64: Base64 encoded audio data
393
- student_id: The student's unique identifier
394
-
395
- Returns:
396
- Transcription and response
397
- """
398
- # Process voice input
399
- result = process_voice_input(audio_data_base64)
400
-
401
- # Process the transcription as a text query
402
- text_response = process_text_query(result["transcription"], {"student_id": student_id})
403
-
404
- # Generate speech response
405
- speech_response = generate_speech_response(
406
- text_response["response"],
407
- {"voice_id": "educational_tutor"}
408
- )
409
-
410
- # Combine results
411
- return {
412
- "input_transcription": result["transcription"],
413
- "input_confidence": result["confidence"],
414
- "detected_emotions": result.get("detected_emotions", {}),
415
- "text_response": text_response["response"],
416
- "speech_response": speech_response,
417
- "timestamp": datetime.now().isoformat()
418
- }
419
-
420
- @mcp.tool()
421
- def handwriting_recognition(image_data_base64: str, student_id: str) -> Dict[str, Any]:
422
- """
423
- Process handwritten input from the student
424
-
425
- Args:
426
- image_data_base64: Base64 encoded image data of handwriting
427
- student_id: The student's unique identifier
428
-
429
- Returns:
430
- Transcription and analysis
431
- """
432
- # Process handwriting input
433
- result = process_handwriting(image_data_base64)
434
-
435
- # If it's a math equation, solve it
436
- if result["detected_content_type"] == "math_equation":
437
- # In a real implementation, this would use a math engine to solve the equation
438
- # For demonstration, we'll provide a simulated solution
439
- if result["equation_type"] == "quadratic":
440
- solution = {
441
- "equation": result["transcription"],
442
- "solution_steps": [
443
- "x^2 + 5x + 6 = 0",
444
- "Factor: (x + 2)(x + 3) = 0",
445
- "x + 2 = 0 or x + 3 = 0",
446
- "x = -2 or x = -3"
447
- ],
448
- "solutions": [-2, -3]
449
- }
450
- else:
451
- solution = {
452
- "equation": result["transcription"],
453
- "note": "Solution not implemented for this equation type"
454
- }
455
- else:
456
- solution = None
457
-
458
- return {
459
- "transcription": result["transcription"],
460
- "confidence": result["confidence"],
461
- "detected_content_type": result["detected_content_type"],
462
- "solution": solution,
463
- "timestamp": datetime.now().isoformat()
464
- }
465
-
466
- # ------------------ Advanced Assessment Tools ------------------
467
-
468
- @mcp.tool()
469
- def create_assessment(concept_ids: List[str], num_questions: int, difficulty: int = 3) -> Dict[str, Any]:
470
- """
471
- Create a complete assessment for given concepts
472
-
473
- Args:
474
- concept_ids: List of concept IDs to include
475
- num_questions: Number of questions to generate
476
- difficulty: Difficulty level (1-5)
477
-
478
- Returns:
479
- Complete assessment with questions
480
- """
481
- questions = []
482
-
483
- # Distribute questions evenly among concepts
484
- questions_per_concept = num_questions // len(concept_ids)
485
- extra_questions = num_questions % len(concept_ids)
486
-
487
- for i, concept_id in enumerate(concept_ids):
488
- # Determine how many questions for this concept
489
- concept_questions = questions_per_concept
490
- if i < extra_questions:
491
- concept_questions += 1
492
-
493
- # Generate questions for this concept
494
- for _ in range(concept_questions):
495
- questions.append(generate_question(concept_id, difficulty))
496
-
497
- return {
498
- "assessment_id": f"assessment_{datetime.now().strftime('%Y%m%d%H%M%S')}",
499
- "concept_ids": concept_ids,
500
- "difficulty": difficulty,
501
- "num_questions": len(questions),
502
- "questions": questions,
503
- "created_at": datetime.now().isoformat()
504
- }
505
 
506
- @mcp.tool()
507
- def grade_assessment(assessment_id: str, student_answers: Dict[str, str], questions: List[Dict[str, Any]]) -> Dict[str, Any]:
508
- """
509
- Grade a completed assessment
510
-
511
- Args:
512
- assessment_id: The ID of the assessment
513
- student_answers: Dictionary mapping question IDs to student answers
514
- questions: List of question objects
515
-
516
- Returns:
517
- Grading results
518
- """
519
- results = []
520
- correct_count = 0
521
-
522
- for question in questions:
523
- question_id = question["id"]
524
- if question_id in student_answers:
525
- evaluation = evaluate_student_answer(question, student_answers[question_id])
526
- results.append(evaluation)
527
- if evaluation["is_correct"]:
528
- correct_count += 1
529
-
530
- # Calculate score
531
- score = correct_count / len(questions) if questions else 0
532
-
533
- # Analyze error patterns
534
- error_types = {}
535
- for result in results:
536
- if result["error_type"]:
537
- error_type = result["error_type"]
538
- error_types[error_type] = error_types.get(error_type, 0) + 1
539
-
540
- # Find most common error
541
- most_common_error = None
542
- if error_types:
543
- most_common_error = max(error_types.items(), key=lambda x: x[1])
544
-
545
- return {
546
- "assessment_id": assessment_id,
547
- "score": score,
548
- "correct_count": correct_count,
549
- "total_questions": len(questions),
550
- "results": results,
551
- "most_common_error": most_common_error,
552
- "completed_at": datetime.now().isoformat()
553
- }
554
 
555
- @mcp.tool()
556
- def get_student_analytics(student_id: str, timeframe_days: int = 30) -> Dict[str, Any]:
557
- """
558
- Get comprehensive analytics for a student
559
-
560
- Args:
561
- student_id: The student's unique identifier
562
- timeframe_days: Number of days to include in analysis
563
-
564
- Returns:
565
- Performance analytics
566
- """
567
- return generate_performance_analytics(student_id, timeframe_days)
568
 
569
- @mcp.tool()
570
- def check_submission_originality(submission: str, reference_sources: List[str]) -> Dict[str, Any]:
571
- """
572
- Check student submission for potential plagiarism
573
-
574
- Args:
575
- submission: The student's submission text
576
- reference_sources: List of reference texts to check against
577
-
578
- Returns:
579
- Originality analysis
580
- """
581
- return detect_plagiarism(submission, reference_sources)
582
 
583
  if __name__ == "__main__":
584
- mcp.run()
 
2
  from mcp.server.fastmcp import FastMCP
3
  import json
4
  import os
5
+ import warnings
6
  from typing import List, Dict, Any, Optional
7
  from datetime import datetime
8
+ from fastapi import FastAPI, Query
9
+ from fastapi.responses import JSONResponse
10
+
11
+ # Filter out the tool registration warning
12
+ warnings.filterwarnings("ignore", message="Tool already exists")
13
 
14
  # Import utility functions
15
  from utils.multimodal import (
 
25
  detect_plagiarism
26
  )
27
 
28
+ # Get server configuration from environment variables with defaults
29
+ SERVER_HOST = os.getenv("MCP_HOST", "0.0.0.0") # Allow connections from any IP
30
+ SERVER_PORT = int(os.getenv("MCP_PORT", "8001"))
31
+ SERVER_TRANSPORT = os.getenv("MCP_TRANSPORT", "http")
32
+
33
+ # Create the TutorX MCP server with explicit configuration
34
+ mcp = FastMCP(
35
+ "TutorX",
36
+ dependencies=["mcp[cli]>=1.9.3", "gradio>=4.19.0", "numpy>=1.24.0", "pillow>=10.0.0"],
37
+ host=SERVER_HOST,
38
+ port=SERVER_PORT,
39
+ transport=SERVER_TRANSPORT,
40
+ cors_origins=["*"] # Allow CORS from any origin
41
+ )
42
+
43
+ # Create FastAPI app
44
+ api_app = FastAPI()
45
 
46
  # ------------------ Core Features ------------------
47
 
48
  # Adaptive Learning Engine
49
  @mcp.tool()
50
+ async def assess_skill(student_id: str, concept_id: str) -> Dict[str, Any]:
51
  """
52
  Assess student's skill level on a specific concept
53
 
 
58
  Returns:
59
  Dictionary containing skill level and recommendations
60
  """
61
+ try:
62
+ # Simulated skill assessment
63
+ return {
64
+ "student_id": student_id,
65
+ "concept_id": concept_id,
66
+ "skill_level": 0.75,
67
+ "confidence": 0.85,
68
+ "recommendations": [
69
+ "Practice more complex problems",
70
+ "Review related concept: algebra_linear_equations"
71
+ ],
72
+ "timestamp": datetime.now().isoformat()
73
+ }
74
+ except Exception as e:
75
+ return {
76
+ "error": str(e),
77
+ "timestamp": datetime.now().isoformat()
78
+ }
79
 
80
  @mcp.resource("concept-graph://")
81
+ async def get_concept_graph() -> Dict[str, Any]:
82
  """Get the full knowledge concept graph"""
83
  return {
84
  "nodes": [
 
93
  }
94
 
95
  @mcp.resource("learning-path://{student_id}")
96
+ async def get_learning_path(student_id: str) -> Dict[str, Any]:
97
  """Get personalized learning path for a student"""
98
  return {
99
  "student_id": student_id,
 
105
 
106
  # Assessment Suite
107
  @mcp.tool()
108
+ async def generate_quiz(concept_ids: List[str], difficulty: int = 2) -> Dict[str, Any]:
109
  """
110
  Generate a quiz based on specified concepts and difficulty
111
 
 
136
  ]
137
  }
138
 
139
+ @api_app.get("/api/assess_skill")
140
+ async def assess_skill_api(student_id: str = Query(...), concept_id: str = Query(...)):
141
+ result = await assess_skill(student_id, concept_id)
142
+ return JSONResponse(content=result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ @api_app.post("/api/generate_quiz")
145
+ async def generate_quiz_api(concept_ids: list[str], difficulty: int = 2):
146
+ result = await generate_quiz(concept_ids, difficulty)
147
+ return JSONResponse(content=result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ # Mount FastAPI app to MCP server
150
+ mcp.app = api_app
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ # Function to run the server
153
+ def run_server():
154
+ """Run the MCP server with configured transport and port"""
155
+ print(f"Starting TutorX MCP Server on {SERVER_HOST}:{SERVER_PORT} using {SERVER_TRANSPORT} transport...")
156
+ try:
157
+ mcp.run(transport="sse")
158
+ except Exception as e:
159
+ print(f"Error starting server: {str(e)}")
160
+ raise
 
 
 
 
161
 
162
  if __name__ == "__main__":
163
+ run_server()
pyproject.toml CHANGED
@@ -6,18 +6,25 @@ readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
8
  "mcp[cli]>=1.9.3",
 
 
 
9
  "gradio>=4.19.0",
10
  "numpy>=1.24.0",
11
  "pillow>=10.0.0",
12
- "requests>=2.31.0",
 
13
  ]
14
 
15
  [project.optional-dependencies]
16
  test = [
17
  "pytest>=7.4.0",
18
  "pytest-cov>=4.1.0",
 
 
19
  ]
20
 
21
  [tool.pytest.ini_options]
22
  testpaths = ["tests"]
23
  python_files = "test_*.py"
 
 
6
  requires-python = ">=3.12"
7
  dependencies = [
8
  "mcp[cli]>=1.9.3",
9
+ "fastapi>=0.109.0",
10
+ "uvicorn>=0.27.0",
11
+ "aiohttp>=3.9.0",
12
  "gradio>=4.19.0",
13
  "numpy>=1.24.0",
14
  "pillow>=10.0.0",
15
+ "python-multipart>=0.0.6",
16
+ "pydantic>=2.6.0",
17
  ]
18
 
19
  [project.optional-dependencies]
20
  test = [
21
  "pytest>=7.4.0",
22
  "pytest-cov>=4.1.0",
23
+ "pytest-asyncio>=0.23.0",
24
+ "httpx>=0.26.0",
25
  ]
26
 
27
  [tool.pytest.ini_options]
28
  testpaths = ["tests"]
29
  python_files = "test_*.py"
30
+ asyncio_mode = "auto"
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.68.0
2
+ uvicorn>=0.15.0
3
+ aiohttp>=3.8.0
4
+ python-multipart>=0.0.5
5
+ pydantic>=1.8.0
6
+ gradio>=4.19.0
7
+ numpy>=1.24.0
8
+ pillow>=10.0.0
9
+ python-jose[cryptography]>=3.3.0
10
+ passlib[bcrypt]>=1.7.4
11
+ python-dotenv>=0.19.0
12
+ httpx>=0.24.0
13
+ pytest>=7.0.0
14
+ pytest-asyncio>=0.18.0
15
+ pytest-cov>=3.0.0
16
+ black>=22.0.0
17
+ isort>=5.10.0
18
+ mypy>=0.910
19
+ ruff>=0.0.262
run.py CHANGED
@@ -6,6 +6,8 @@ import argparse
6
  import importlib.util
7
  import os
8
  import sys
 
 
9
 
10
  def load_module(name, path):
11
  """Load a module from path"""
@@ -14,16 +16,22 @@ def load_module(name, path):
14
  spec.loader.exec_module(module)
15
  return module
16
 
17
- def run_mcp_server():
18
- """Run the MCP server"""
19
- print("Starting TutorX MCP Server...")
 
 
 
 
 
 
20
  main_module = load_module("main", "main.py")
21
 
22
  # Access the mcp instance and run it
23
- if hasattr(main_module, "mcp"):
24
- main_module.mcp.run()
25
  else:
26
- print("Error: MCP server instance not found in main.py")
27
  sys.exit(1)
28
 
29
  def run_gradio_interface():
@@ -38,6 +46,17 @@ def run_gradio_interface():
38
  print("Error: Gradio demo not found in app.py")
39
  sys.exit(1)
40
 
 
 
 
 
 
 
 
 
 
 
 
41
  if __name__ == "__main__":
42
  parser = argparse.ArgumentParser(description="Run TutorX MCP Server or Gradio Interface")
43
  parser.add_argument(
@@ -57,24 +76,36 @@ if __name__ == "__main__":
57
  default=8000,
58
  help="Port to use"
59
  )
 
 
 
 
 
 
60
 
61
  args = parser.parse_args()
62
 
63
  if args.mode == "mcp":
64
- # Set environment variables for MCP server
65
- os.environ["MCP_HOST"] = args.host
66
- os.environ["MCP_PORT"] = str(args.port)
67
- run_mcp_server()
68
  elif args.mode == "gradio":
69
  run_gradio_interface()
70
  elif args.mode == "both":
71
  # For 'both' mode, we'll start MCP server in a separate process
72
- import subprocess
73
- import time
74
-
75
  # Start MCP server in a background process
76
  mcp_process = subprocess.Popen(
77
- [sys.executable, "run.py", "--mode", "mcp", "--host", args.host, "--port", str(args.port)],
 
 
 
 
 
 
 
78
  stdout=subprocess.PIPE,
79
  stderr=subprocess.PIPE
80
  )
 
6
  import importlib.util
7
  import os
8
  import sys
9
+ import time
10
+ import subprocess
11
 
12
  def load_module(name, path):
13
  """Load a module from path"""
 
16
  spec.loader.exec_module(module)
17
  return module
18
 
19
+ def run_mcp_server(host="127.0.0.1", port=8000, transport="streamable-http"):
20
+ """Run the MCP server with specified configuration"""
21
+ print(f"Starting TutorX MCP Server on {host}:{port} using {transport} transport...")
22
+
23
+ # Set environment variables for MCP server
24
+ os.environ["MCP_HOST"] = host
25
+ os.environ["MCP_PORT"] = str(port)
26
+ os.environ["MCP_TRANSPORT"] = transport
27
+
28
  main_module = load_module("main", "main.py")
29
 
30
  # Access the mcp instance and run it
31
+ if hasattr(main_module, "run_server"):
32
+ main_module.run_server()
33
  else:
34
+ print("Error: run_server function not found in main.py")
35
  sys.exit(1)
36
 
37
  def run_gradio_interface():
 
46
  print("Error: Gradio demo not found in app.py")
47
  sys.exit(1)
48
 
49
+ def check_port_available(port):
50
+ """Check if a port is available"""
51
+ import socket
52
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
53
+ try:
54
+ sock.bind(('127.0.0.1', port))
55
+ sock.close()
56
+ return True
57
+ except:
58
+ return False
59
+
60
  if __name__ == "__main__":
61
  parser = argparse.ArgumentParser(description="Run TutorX MCP Server or Gradio Interface")
62
  parser.add_argument(
 
76
  default=8000,
77
  help="Port to use"
78
  )
79
+ parser.add_argument(
80
+ "--transport",
81
+ choices=["stdio", "streamable-http", "sse"],
82
+ default="streamable-http",
83
+ help="Transport protocol to use"
84
+ )
85
 
86
  args = parser.parse_args()
87
 
88
  if args.mode == "mcp":
89
+ if not check_port_available(args.port):
90
+ print(f"Warning: Port {args.port} is already in use. Trying to use the server anyway...")
91
+ run_mcp_server(args.host, args.port, args.transport)
 
92
  elif args.mode == "gradio":
93
  run_gradio_interface()
94
  elif args.mode == "both":
95
  # For 'both' mode, we'll start MCP server in a separate process
96
+ if not check_port_available(args.port):
97
+ print(f"Warning: Port {args.port} is already in use. Trying to use the server anyway...")
98
+
99
  # Start MCP server in a background process
100
  mcp_process = subprocess.Popen(
101
+ [
102
+ sys.executable,
103
+ "run.py",
104
+ "--mode", "mcp",
105
+ "--host", args.host,
106
+ "--port", str(args.port),
107
+ "--transport", args.transport
108
+ ],
109
  stdout=subprocess.PIPE,
110
  stderr=subprocess.PIPE
111
  )
uv.lock CHANGED
The diff for this file is too large to render. See raw diff