ragtime-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ragtime-cli might be problematic. Click here for more details.
- ragtime_cli-0.1.0.dist-info/METADATA +220 -0
- ragtime_cli-0.1.0.dist-info/RECORD +21 -0
- ragtime_cli-0.1.0.dist-info/WHEEL +5 -0
- ragtime_cli-0.1.0.dist-info/entry_points.txt +3 -0
- ragtime_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ragtime_cli-0.1.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/cli.py +773 -0
- src/commands/audit.md +151 -0
- src/commands/handoff.md +176 -0
- src/commands/pr-graduate.md +187 -0
- src/commands/recall.md +175 -0
- src/commands/remember.md +168 -0
- src/commands/save.md +10 -0
- src/commands/start.md +206 -0
- src/config.py +101 -0
- src/db.py +167 -0
- src/indexers/__init__.py +0 -0
- src/indexers/docs.py +129 -0
- src/mcp_server.py +590 -0
- src/memory.py +379 -0
src/mcp_server.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server for ragtime.
|
|
3
|
+
|
|
4
|
+
Exposes ragtime operations as MCP tools for Claude integration.
|
|
5
|
+
Run with: python -m src.mcp_server
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .db import RagtimeDB
|
|
15
|
+
from .memory import Memory, MemoryStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RagtimeMCPServer:
|
|
19
|
+
"""MCP Server that exposes ragtime operations as tools."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, project_path: Path | None = None):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the MCP server.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_path: Root of the project (defaults to cwd)
|
|
27
|
+
"""
|
|
28
|
+
self.project_path = project_path or Path.cwd()
|
|
29
|
+
self._db = None
|
|
30
|
+
self._store = None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def db(self) -> RagtimeDB:
|
|
34
|
+
"""Lazy-load the database."""
|
|
35
|
+
if self._db is None:
|
|
36
|
+
db_path = self.project_path / ".ragtime" / "index"
|
|
37
|
+
self._db = RagtimeDB(db_path)
|
|
38
|
+
return self._db
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def store(self) -> MemoryStore:
|
|
42
|
+
"""Lazy-load the memory store."""
|
|
43
|
+
if self._store is None:
|
|
44
|
+
self._store = MemoryStore(self.project_path, self.db)
|
|
45
|
+
return self._store
|
|
46
|
+
|
|
47
|
+
def get_author(self) -> str:
|
|
48
|
+
"""Get the current developer's username."""
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
["gh", "api", "user", "--jq", ".login"],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
timeout=5,
|
|
55
|
+
)
|
|
56
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
57
|
+
return result.stdout.strip()
|
|
58
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "config", "user.name"],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
timeout=5,
|
|
67
|
+
)
|
|
68
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
69
|
+
return result.stdout.strip().lower().replace(" ", "-")
|
|
70
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
return "unknown"
|
|
74
|
+
|
|
75
|
+
def get_tools(self) -> list[dict]:
|
|
76
|
+
"""Return the list of available tools."""
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
"name": "remember",
|
|
80
|
+
"description": "Store a memory with structured metadata",
|
|
81
|
+
"inputSchema": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"content": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "The memory content to store"
|
|
87
|
+
},
|
|
88
|
+
"namespace": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Namespace: app, team, user-{name}, branch-{name}"
|
|
91
|
+
},
|
|
92
|
+
"type": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"enum": ["architecture", "feature", "integration", "convention",
|
|
95
|
+
"preference", "decision", "pattern", "task-state", "handoff"],
|
|
96
|
+
"description": "Memory type"
|
|
97
|
+
},
|
|
98
|
+
"component": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Component area (e.g., auth, claims, shifts)"
|
|
101
|
+
},
|
|
102
|
+
"confidence": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"enum": ["high", "medium", "low"],
|
|
105
|
+
"default": "medium",
|
|
106
|
+
"description": "Confidence level"
|
|
107
|
+
},
|
|
108
|
+
"confidence_reason": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Why this confidence level"
|
|
111
|
+
},
|
|
112
|
+
"source": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"default": "remember",
|
|
115
|
+
"description": "Source of this memory"
|
|
116
|
+
},
|
|
117
|
+
"issue": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "Related GitHub issue (e.g., #301)"
|
|
120
|
+
},
|
|
121
|
+
"epic": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Parent epic (e.g., #286)"
|
|
124
|
+
},
|
|
125
|
+
"branch": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Related branch name"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"required": ["content", "namespace", "type"]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "search",
|
|
135
|
+
"description": "Semantic search over indexed content and memories",
|
|
136
|
+
"inputSchema": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"query": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"description": "Natural language search query"
|
|
142
|
+
},
|
|
143
|
+
"namespace": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Filter by namespace"
|
|
146
|
+
},
|
|
147
|
+
"type": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"description": "Filter by type (docs, code, architecture, etc.)"
|
|
150
|
+
},
|
|
151
|
+
"component": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"description": "Filter by component"
|
|
154
|
+
},
|
|
155
|
+
"limit": {
|
|
156
|
+
"type": "integer",
|
|
157
|
+
"default": 10,
|
|
158
|
+
"description": "Max results to return"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"required": ["query"]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "store_doc",
|
|
166
|
+
"description": "Store a document verbatim (like handoff.md)",
|
|
167
|
+
"inputSchema": {
|
|
168
|
+
"type": "object",
|
|
169
|
+
"properties": {
|
|
170
|
+
"content": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"description": "The document content to store"
|
|
173
|
+
},
|
|
174
|
+
"namespace": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "Namespace for the document"
|
|
177
|
+
},
|
|
178
|
+
"doc_type": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"enum": ["handoff", "document", "plan", "notes"],
|
|
181
|
+
"default": "handoff",
|
|
182
|
+
"description": "Document type"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"required": ["content", "namespace"]
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"name": "list_memories",
|
|
190
|
+
"description": "List memories with optional filters",
|
|
191
|
+
"inputSchema": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"properties": {
|
|
194
|
+
"namespace": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"description": "Filter by namespace (use * suffix for prefix match)"
|
|
197
|
+
},
|
|
198
|
+
"type": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"description": "Filter by type"
|
|
201
|
+
},
|
|
202
|
+
"status": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"enum": ["active", "graduated", "abandoned", "ungraduated"],
|
|
205
|
+
"description": "Filter by status"
|
|
206
|
+
},
|
|
207
|
+
"component": {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"description": "Filter by component"
|
|
210
|
+
},
|
|
211
|
+
"limit": {
|
|
212
|
+
"type": "integer",
|
|
213
|
+
"default": 20,
|
|
214
|
+
"description": "Max results"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "get_memory",
|
|
221
|
+
"description": "Get a specific memory by ID",
|
|
222
|
+
"inputSchema": {
|
|
223
|
+
"type": "object",
|
|
224
|
+
"properties": {
|
|
225
|
+
"memory_id": {
|
|
226
|
+
"type": "string",
|
|
227
|
+
"description": "The memory ID"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"required": ["memory_id"]
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "forget",
|
|
235
|
+
"description": "Delete a memory by ID",
|
|
236
|
+
"inputSchema": {
|
|
237
|
+
"type": "object",
|
|
238
|
+
"properties": {
|
|
239
|
+
"memory_id": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"description": "The memory ID to delete"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"required": ["memory_id"]
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "graduate",
|
|
249
|
+
"description": "Graduate a branch memory to app namespace",
|
|
250
|
+
"inputSchema": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {
|
|
253
|
+
"memory_id": {
|
|
254
|
+
"type": "string",
|
|
255
|
+
"description": "The memory ID to graduate"
|
|
256
|
+
},
|
|
257
|
+
"confidence": {
|
|
258
|
+
"type": "string",
|
|
259
|
+
"enum": ["high", "medium", "low"],
|
|
260
|
+
"default": "high",
|
|
261
|
+
"description": "Confidence level for graduated memory"
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"required": ["memory_id"]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "update_status",
|
|
269
|
+
"description": "Update a memory's status (e.g., mark as abandoned)",
|
|
270
|
+
"inputSchema": {
|
|
271
|
+
"type": "object",
|
|
272
|
+
"properties": {
|
|
273
|
+
"memory_id": {
|
|
274
|
+
"type": "string",
|
|
275
|
+
"description": "The memory ID"
|
|
276
|
+
},
|
|
277
|
+
"status": {
|
|
278
|
+
"type": "string",
|
|
279
|
+
"enum": ["active", "graduated", "abandoned", "ungraduated"],
|
|
280
|
+
"description": "New status"
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"required": ["memory_id", "status"]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
def call_tool(self, name: str, arguments: dict) -> Any:
|
|
289
|
+
"""Execute a tool call."""
|
|
290
|
+
if name == "remember":
|
|
291
|
+
return self._remember(arguments)
|
|
292
|
+
elif name == "search":
|
|
293
|
+
return self._search(arguments)
|
|
294
|
+
elif name == "store_doc":
|
|
295
|
+
return self._store_doc(arguments)
|
|
296
|
+
elif name == "list_memories":
|
|
297
|
+
return self._list_memories(arguments)
|
|
298
|
+
elif name == "get_memory":
|
|
299
|
+
return self._get_memory(arguments)
|
|
300
|
+
elif name == "forget":
|
|
301
|
+
return self._forget(arguments)
|
|
302
|
+
elif name == "graduate":
|
|
303
|
+
return self._graduate(arguments)
|
|
304
|
+
elif name == "update_status":
|
|
305
|
+
return self._update_status(arguments)
|
|
306
|
+
else:
|
|
307
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
308
|
+
|
|
309
|
+
def _remember(self, args: dict) -> dict:
|
|
310
|
+
"""Store a memory."""
|
|
311
|
+
memory = Memory(
|
|
312
|
+
content=args["content"],
|
|
313
|
+
namespace=args["namespace"],
|
|
314
|
+
type=args["type"],
|
|
315
|
+
component=args.get("component"),
|
|
316
|
+
confidence=args.get("confidence", "medium"),
|
|
317
|
+
confidence_reason=args.get("confidence_reason"),
|
|
318
|
+
source=args.get("source", "remember"),
|
|
319
|
+
author=self.get_author(),
|
|
320
|
+
issue=args.get("issue"),
|
|
321
|
+
epic=args.get("epic"),
|
|
322
|
+
branch=args.get("branch"),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
file_path = self.store.save(memory)
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"success": True,
|
|
329
|
+
"memory_id": memory.id,
|
|
330
|
+
"file": str(file_path.relative_to(self.project_path)),
|
|
331
|
+
"namespace": memory.namespace,
|
|
332
|
+
"type": memory.type,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
def _search(self, args: dict) -> dict:
|
|
336
|
+
"""Search indexed content."""
|
|
337
|
+
results = self.db.search(
|
|
338
|
+
query=args["query"],
|
|
339
|
+
limit=args.get("limit", 10),
|
|
340
|
+
namespace=args.get("namespace"),
|
|
341
|
+
type_filter=args.get("type"),
|
|
342
|
+
component=args.get("component"),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"count": len(results),
|
|
347
|
+
"results": [
|
|
348
|
+
{
|
|
349
|
+
"content": r["content"],
|
|
350
|
+
"metadata": r["metadata"],
|
|
351
|
+
"score": 1 - r["distance"] if r["distance"] else None,
|
|
352
|
+
}
|
|
353
|
+
for r in results
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def _store_doc(self, args: dict) -> dict:
|
|
358
|
+
"""Store a document verbatim."""
|
|
359
|
+
memory = Memory(
|
|
360
|
+
content=args["content"],
|
|
361
|
+
namespace=args["namespace"],
|
|
362
|
+
type=args.get("doc_type", "handoff"),
|
|
363
|
+
source="store-doc",
|
|
364
|
+
confidence="medium",
|
|
365
|
+
confidence_reason="document",
|
|
366
|
+
author=self.get_author(),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
file_path = self.store.save(memory)
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"success": True,
|
|
373
|
+
"memory_id": memory.id,
|
|
374
|
+
"file": str(file_path.relative_to(self.project_path)),
|
|
375
|
+
"namespace": memory.namespace,
|
|
376
|
+
"type": memory.type,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def _list_memories(self, args: dict) -> dict:
|
|
380
|
+
"""List memories with filters."""
|
|
381
|
+
memories = self.store.list_memories(
|
|
382
|
+
namespace=args.get("namespace"),
|
|
383
|
+
type_filter=args.get("type"),
|
|
384
|
+
status=args.get("status"),
|
|
385
|
+
component=args.get("component"),
|
|
386
|
+
limit=args.get("limit", 20),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
"count": len(memories),
|
|
391
|
+
"memories": [
|
|
392
|
+
{
|
|
393
|
+
"id": m.id,
|
|
394
|
+
"namespace": m.namespace,
|
|
395
|
+
"type": m.type,
|
|
396
|
+
"component": m.component,
|
|
397
|
+
"status": m.status,
|
|
398
|
+
"confidence": m.confidence,
|
|
399
|
+
"added": m.added,
|
|
400
|
+
"source": m.source,
|
|
401
|
+
"preview": m.content[:100],
|
|
402
|
+
}
|
|
403
|
+
for m in memories
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
def _get_memory(self, args: dict) -> dict:
|
|
408
|
+
"""Get a specific memory."""
|
|
409
|
+
memory = self.store.get(args["memory_id"])
|
|
410
|
+
|
|
411
|
+
if not memory:
|
|
412
|
+
return {"success": False, "error": "Memory not found"}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
"success": True,
|
|
416
|
+
"memory": {
|
|
417
|
+
"id": memory.id,
|
|
418
|
+
"content": memory.content,
|
|
419
|
+
"namespace": memory.namespace,
|
|
420
|
+
"type": memory.type,
|
|
421
|
+
"component": memory.component,
|
|
422
|
+
"status": memory.status,
|
|
423
|
+
"confidence": memory.confidence,
|
|
424
|
+
"confidence_reason": memory.confidence_reason,
|
|
425
|
+
"added": memory.added,
|
|
426
|
+
"author": memory.author,
|
|
427
|
+
"source": memory.source,
|
|
428
|
+
"issue": memory.issue,
|
|
429
|
+
"epic": memory.epic,
|
|
430
|
+
"branch": memory.branch,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
def _forget(self, args: dict) -> dict:
|
|
435
|
+
"""Delete a memory."""
|
|
436
|
+
success = self.store.delete(args["memory_id"])
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
"success": success,
|
|
440
|
+
"memory_id": args["memory_id"],
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
def _graduate(self, args: dict) -> dict:
|
|
444
|
+
"""Graduate a branch memory."""
|
|
445
|
+
try:
|
|
446
|
+
graduated = self.store.graduate(
|
|
447
|
+
args["memory_id"],
|
|
448
|
+
args.get("confidence", "high"),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if not graduated:
|
|
452
|
+
return {"success": False, "error": "Memory not found"}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"success": True,
|
|
456
|
+
"original_id": args["memory_id"],
|
|
457
|
+
"graduated_id": graduated.id,
|
|
458
|
+
"namespace": graduated.namespace,
|
|
459
|
+
}
|
|
460
|
+
except ValueError as e:
|
|
461
|
+
return {"success": False, "error": str(e)}
|
|
462
|
+
|
|
463
|
+
def _update_status(self, args: dict) -> dict:
|
|
464
|
+
"""Update a memory's status."""
|
|
465
|
+
success = self.store.update_status(
|
|
466
|
+
args["memory_id"],
|
|
467
|
+
args["status"],
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
"success": success,
|
|
472
|
+
"memory_id": args["memory_id"],
|
|
473
|
+
"status": args["status"],
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
def handle_message(self, message: dict) -> dict:
|
|
477
|
+
"""Handle an incoming JSON-RPC message."""
|
|
478
|
+
method = message.get("method")
|
|
479
|
+
msg_id = message.get("id")
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
if method == "initialize":
|
|
483
|
+
return {
|
|
484
|
+
"jsonrpc": "2.0",
|
|
485
|
+
"id": msg_id,
|
|
486
|
+
"result": {
|
|
487
|
+
"protocolVersion": "2024-11-05",
|
|
488
|
+
"serverInfo": {
|
|
489
|
+
"name": "ragtime",
|
|
490
|
+
"version": "0.1.0",
|
|
491
|
+
},
|
|
492
|
+
"capabilities": {
|
|
493
|
+
"tools": {},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
elif method == "notifications/initialized":
|
|
499
|
+
# No response needed for notifications
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
elif method == "tools/list":
|
|
503
|
+
return {
|
|
504
|
+
"jsonrpc": "2.0",
|
|
505
|
+
"id": msg_id,
|
|
506
|
+
"result": {
|
|
507
|
+
"tools": self.get_tools(),
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
elif method == "tools/call":
|
|
512
|
+
params = message.get("params", {})
|
|
513
|
+
tool_name = params.get("name")
|
|
514
|
+
arguments = params.get("arguments", {})
|
|
515
|
+
|
|
516
|
+
result = self.call_tool(tool_name, arguments)
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
"jsonrpc": "2.0",
|
|
520
|
+
"id": msg_id,
|
|
521
|
+
"result": {
|
|
522
|
+
"content": [
|
|
523
|
+
{
|
|
524
|
+
"type": "text",
|
|
525
|
+
"text": json.dumps(result, indent=2),
|
|
526
|
+
}
|
|
527
|
+
],
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
else:
|
|
532
|
+
return {
|
|
533
|
+
"jsonrpc": "2.0",
|
|
534
|
+
"id": msg_id,
|
|
535
|
+
"error": {
|
|
536
|
+
"code": -32601,
|
|
537
|
+
"message": f"Method not found: {method}",
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
return {
|
|
543
|
+
"jsonrpc": "2.0",
|
|
544
|
+
"id": msg_id,
|
|
545
|
+
"error": {
|
|
546
|
+
"code": -32603,
|
|
547
|
+
"message": str(e),
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
def run(self):
|
|
552
|
+
"""Run the MCP server on stdin/stdout."""
|
|
553
|
+
while True:
|
|
554
|
+
try:
|
|
555
|
+
line = sys.stdin.readline()
|
|
556
|
+
if not line:
|
|
557
|
+
break
|
|
558
|
+
|
|
559
|
+
message = json.loads(line)
|
|
560
|
+
response = self.handle_message(message)
|
|
561
|
+
|
|
562
|
+
if response is not None:
|
|
563
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
564
|
+
sys.stdout.flush()
|
|
565
|
+
|
|
566
|
+
except json.JSONDecodeError:
|
|
567
|
+
continue
|
|
568
|
+
except KeyboardInterrupt:
|
|
569
|
+
break
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def main():
|
|
573
|
+
"""Entry point for the MCP server."""
|
|
574
|
+
import argparse
|
|
575
|
+
|
|
576
|
+
parser = argparse.ArgumentParser(description="Ragtime MCP Server")
|
|
577
|
+
parser.add_argument(
|
|
578
|
+
"--path",
|
|
579
|
+
type=Path,
|
|
580
|
+
default=Path.cwd(),
|
|
581
|
+
help="Project path (defaults to current directory)",
|
|
582
|
+
)
|
|
583
|
+
args = parser.parse_args()
|
|
584
|
+
|
|
585
|
+
server = RagtimeMCPServer(args.path)
|
|
586
|
+
server.run()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
if __name__ == "__main__":
|
|
590
|
+
main()
|