bigwolfe commited on
Commit
bc942ec
·
1 Parent(s): 61c13df

Update MCP server configuration

Browse files
Files changed (1) hide show
  1. backend/src/mcp/server.py +144 -18
backend/src/mcp/server.py CHANGED
@@ -16,7 +16,7 @@ load_dotenv()
16
 
17
  from ..services import IndexerService, VaultNote, VaultService
18
  from ..services.auth import AuthError, AuthService
19
- from ..services.config import get_config
20
 
21
  try:
22
  from fastmcp.server.http import _current_http_request # type: ignore
@@ -42,6 +42,38 @@ indexer_service = IndexerService()
42
  auth_service = AuthService()
43
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def _current_user_id() -> str:
46
  """Resolve the acting user ID (local mode defaults to local-dev)."""
47
  # HTTP transport (hosted) uses Authorization headers
@@ -120,12 +152,24 @@ def list_notes(
120
  ]
121
 
122
 
123
- @mcp.tool(name="read_note", description="Read a Markdown note with metadata and body.")
 
 
 
 
 
 
 
 
 
 
 
 
124
  def read_note(
125
  path: str = Field(
126
  ..., description="Relative '.md' path ≤256 chars (no '..' or '\\')."
127
  ),
128
- ) -> Dict[str, Any]:
129
  start_time = time.time()
130
  user_id = _current_user_id()
131
 
@@ -142,12 +186,45 @@ def read_note(
142
  },
143
  )
144
 
145
- return _note_to_response(note)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
 
148
  @mcp.tool(
149
  name="write_note",
150
  description="Create or update a note. Automatically updates frontmatter timestamps and search index.",
 
 
 
 
 
 
 
 
 
151
  )
152
  def write_note(
153
  path: str = Field(
@@ -175,9 +252,6 @@ def write_note(
175
  )
176
  indexer_service.index_note(user_id, note)
177
 
178
- config = get_config()
179
- widget_url = f"{config.hf_space_url}/widget.html"
180
-
181
  duration_ms = (time.time() - start_time) * 1000
182
  logger.info(
183
  "MCP tool called",
@@ -209,11 +283,7 @@ def write_note(
209
  },
210
  "_meta": {
211
  "openai": {
212
- "outputTemplate": widget_url,
213
- "toolInvocation": {
214
- "invoking": f"Saving {path}...",
215
- "invoked": f"Saved {path}"
216
- }
217
  }
218
  },
219
  "isError": False
@@ -246,13 +316,69 @@ def delete_note(
246
  return {"status": "ok"}
247
 
248
 
249
- @mcp.tool()
250
- def search_notes(query: str) -> list[str]:
251
- """Search for notes in the vault."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  user_id = _current_user_id()
253
- indexer = IndexerService()
254
- results = indexer.search_notes(user_id, query)
255
- return [f"{r['title']} ({r['path']})" for r in results]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
 
258
 
 
16
 
17
  from ..services import IndexerService, VaultNote, VaultService
18
  from ..services.auth import AuthError, AuthService
19
+ from ..services.config import get_config, PROJECT_ROOT
20
 
21
  try:
22
  from fastmcp.server.http import _current_http_request # type: ignore
 
42
  auth_service = AuthService()
43
 
44
 
45
+ @mcp.resource("widget")
46
+ def widget_resource() -> dict:
47
+ """Return the widget HTML bundle."""
48
+ # Locate widget.html relative to project root
49
+ # In Docker: /app/frontend/dist/widget.html
50
+ # Local: frontend/dist/widget.html
51
+ # We use PROJECT_ROOT from config
52
+
53
+ widget_path = PROJECT_ROOT / "frontend" / "dist" / "widget.html"
54
+
55
+ if not widget_path.exists():
56
+ # Fallback for local dev if not built? Or just error.
57
+ # Apps SDK expects specific structure.
58
+ return {
59
+ "contents": [{
60
+ "uri": "ui://widget/note.html",
61
+ "mimeType": "text/plain",
62
+ "text": "Widget build not found."
63
+ }]
64
+ }
65
+
66
+ html_content = widget_path.read_text(encoding="utf-8")
67
+
68
+ return {
69
+ "contents": [{
70
+ "uri": "ui://widget/note.html",
71
+ "mimeType": "text/html+skybridge",
72
+ "text": html_content
73
+ }]
74
+ }
75
+
76
+
77
  def _current_user_id() -> str:
78
  """Resolve the acting user ID (local mode defaults to local-dev)."""
79
  # HTTP transport (hosted) uses Authorization headers
 
152
  ]
153
 
154
 
155
+ @mcp.tool(
156
+ name="read_note",
157
+ description="Read a Markdown note with metadata and body.",
158
+ _meta={
159
+ "openai": {
160
+ "outputTemplate": "ui://widget/note.html",
161
+ "toolInvocation": {
162
+ "invoking": "Opening note...",
163
+ "invoked": "Note opened."
164
+ }
165
+ }
166
+ }
167
+ )
168
  def read_note(
169
  path: str = Field(
170
  ..., description="Relative '.md' path ≤256 chars (no '..' or '\\')."
171
  ),
172
+ ) -> dict:
173
  start_time = time.time()
174
  user_id = _current_user_id()
175
 
 
186
  },
187
  )
188
 
189
+ structured_note = {
190
+ "title": note["title"],
191
+ "note_path": note["path"],
192
+ "body": note["body"],
193
+ "metadata": note["metadata"],
194
+ "updated": note["modified"].isoformat(),
195
+ }
196
+
197
+ return {
198
+ "content": [
199
+ {
200
+ "type": "text",
201
+ "text": f"Read note: {note['title']}"
202
+ }
203
+ ],
204
+ "structuredContent": {
205
+ "note": structured_note
206
+ },
207
+ "_meta": {
208
+ "openai": {
209
+ "outputTemplate": "ui://widget/note.html"
210
+ }
211
+ },
212
+ "isError": False
213
+ }
214
 
215
 
216
  @mcp.tool(
217
  name="write_note",
218
  description="Create or update a note. Automatically updates frontmatter timestamps and search index.",
219
+ _meta={
220
+ "openai": {
221
+ "outputTemplate": "ui://widget/note.html",
222
+ "toolInvocation": {
223
+ "invoking": "Saving note...",
224
+ "invoked": "Note saved."
225
+ }
226
+ }
227
+ }
228
  )
229
  def write_note(
230
  path: str = Field(
 
252
  )
253
  indexer_service.index_note(user_id, note)
254
 
 
 
 
255
  duration_ms = (time.time() - start_time) * 1000
256
  logger.info(
257
  "MCP tool called",
 
283
  },
284
  "_meta": {
285
  "openai": {
286
+ "outputTemplate": "ui://widget/note.html"
 
 
 
 
287
  }
288
  },
289
  "isError": False
 
316
  return {"status": "ok"}
317
 
318
 
319
+ @mcp.tool(
320
+ name="search_notes",
321
+ description="Full-text search with snippets and recency-aware scoring.",
322
+ _meta={
323
+ "openai": {
324
+ "outputTemplate": "ui://widget/note.html",
325
+ "toolInvocation": {
326
+ "invoking": "Searching...",
327
+ "invoked": "Search complete."
328
+ }
329
+ }
330
+ }
331
+ )
332
+ def search_notes(
333
+ query: str = Field(..., description="Non-empty search query (bm25 + recency)."),
334
+ limit: int = Field(50, ge=1, le=100, description="Result cap between 1 and 100."),
335
+ ) -> dict:
336
+ start_time = time.time()
337
  user_id = _current_user_id()
338
+
339
+ results = indexer_service.search_notes(user_id, query, limit=limit)
340
+
341
+ duration_ms = (time.time() - start_time) * 1000
342
+ logger.info(
343
+ "MCP tool called",
344
+ extra={
345
+ "tool_name": "search_notes",
346
+ "user_id": user_id,
347
+ "query": query,
348
+ "limit": limit,
349
+ "result_count": len(results),
350
+ "duration_ms": f"{duration_ms:.2f}",
351
+ },
352
+ )
353
+
354
+ # Structure results for the widget
355
+ structured_results = []
356
+ for r in results:
357
+ structured_results.append({
358
+ "title": r["title"],
359
+ "note_path": r["path"],
360
+ "snippet": r["snippet"],
361
+ "score": r["score"],
362
+ "updated": r["updated"] if isinstance(r["updated"], str) else r["updated"].isoformat()
363
+ })
364
+
365
+ return {
366
+ "content": [
367
+ {
368
+ "type": "text",
369
+ "text": f"Found {len(results)} notes matching '{query}'."
370
+ }
371
+ ],
372
+ "structuredContent": {
373
+ "results": structured_results
374
+ },
375
+ "_meta": {
376
+ "openai": {
377
+ "outputTemplate": "ui://widget/note.html"
378
+ }
379
+ },
380
+ "isError": False
381
+ }
382
 
383
 
384