docsif 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.
Files changed (86) hide show
  1. docsif-0.1.0.dist-info/METADATA +393 -0
  2. docsif-0.1.0.dist-info/RECORD +86 -0
  3. docsif-0.1.0.dist-info/WHEEL +4 -0
  4. docsif-0.1.0.dist-info/entry_points.txt +3 -0
  5. docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. sif/__init__.py +24 -0
  7. sif/_version.py +3 -0
  8. sif/cli/__init__.py +11 -0
  9. sif/cli/commands/__init__.py +18 -0
  10. sif/cli/commands/bench.py +148 -0
  11. sif/cli/commands/collection.py +360 -0
  12. sif/cli/commands/context.py +154 -0
  13. sif/cli/commands/get.py +200 -0
  14. sif/cli/commands/index.py +294 -0
  15. sif/cli/commands/ls.py +67 -0
  16. sif/cli/commands/mcp.py +93 -0
  17. sif/cli/commands/pull.py +111 -0
  18. sif/cli/commands/search.py +574 -0
  19. sif/cli/config.py +206 -0
  20. sif/cli/formatters.py +269 -0
  21. sif/cli/main.py +205 -0
  22. sif/config/__init__.py +30 -0
  23. sif/config/constants.py +52 -0
  24. sif/config/settings.py +217 -0
  25. sif/core/__init__.py +21 -0
  26. sif/core/collection.py +161 -0
  27. sif/core/context.py +167 -0
  28. sif/core/document.py +125 -0
  29. sif/core/models.py +298 -0
  30. sif/database/__init__.py +23 -0
  31. sif/database/connection.py +139 -0
  32. sif/database/database.py +136 -0
  33. sif/database/migrations.py +187 -0
  34. sif/database/repositories.py +500 -0
  35. sif/database/repository.py +171 -0
  36. sif/database/schema.py +362 -0
  37. sif/embedding/__init__.py +22 -0
  38. sif/embedding/cache.py +196 -0
  39. sif/embedding/embedder.py +328 -0
  40. sif/embedding/factory.py +92 -0
  41. sif/embedding/manager.py +200 -0
  42. sif/embedding/model.py +126 -0
  43. sif/indexing/__init__.py +21 -0
  44. sif/indexing/chunker.py +226 -0
  45. sif/indexing/indexer.py +226 -0
  46. sif/indexing/parser.py +192 -0
  47. sif/indexing/scanner.py +148 -0
  48. sif/indexing/watcher.py +156 -0
  49. sif/mcp/README.md +258 -0
  50. sif/mcp/__init__.py +9 -0
  51. sif/mcp/claude_desktop_config.json +16 -0
  52. sif/mcp/cli.py +92 -0
  53. sif/mcp/protocol.py +336 -0
  54. sif/mcp/server.py +433 -0
  55. sif/mcp/server_http.py +54 -0
  56. sif/mcp/test_mcp.py +108 -0
  57. sif/mcp/tools.py +497 -0
  58. sif/mcp/transport_http.py +424 -0
  59. sif/mcp/transport_stdio.py +292 -0
  60. sif/mcp_server/__init__.py +26 -0
  61. sif/mcp_server/handlers.py +68 -0
  62. sif/mcp_server/server.py +58 -0
  63. sif/mcp_server/tools.py +216 -0
  64. sif/mcp_server/transport.py +221 -0
  65. sif/models/__init__.py +70 -0
  66. sif/models/collection.py +51 -0
  67. sif/models/context.py +45 -0
  68. sif/models/document.py +76 -0
  69. sif/models/download.py +208 -0
  70. sif/models/embedding.py +71 -0
  71. sif/models/search.py +92 -0
  72. sif/search/__init__.py +22 -0
  73. sif/search/benchmark.py +110 -0
  74. sif/search/bm25.py +216 -0
  75. sif/search/expansion.py +157 -0
  76. sif/search/hybrid.py +323 -0
  77. sif/search/rerank.py +390 -0
  78. sif/search/rrf.py +186 -0
  79. sif/search/snippets.py +130 -0
  80. sif/search/strategy.py +97 -0
  81. sif/search/vector.py +177 -0
  82. sif/utils/__init__.py +26 -0
  83. sif/utils/logging.py +143 -0
  84. sif/utils/paths.py +82 -0
  85. sif/utils/progress.py +83 -0
  86. sif/utils/text.py +128 -0
@@ -0,0 +1,424 @@
1
+ """
2
+ MCP HTTP Transport
3
+
4
+ This module implements the HTTP transport for MCP using FastAPI.
5
+ Supports both JSON-RPC POST endpoints and Server-Sent Events (SSE)
6
+ for real-time communication.
7
+
8
+ Endpoints:
9
+ - POST /mcp/v1/messages - JSON-RPC message endpoint
10
+ - GET /mcp/v1/sse - SSE endpoint for server-to-client streaming
11
+ - GET /health - Health check endpoint
12
+ """
13
+
14
+ import asyncio
15
+ import json
16
+ import logging
17
+ from collections.abc import AsyncGenerator
18
+ from datetime import datetime, timezone
19
+ from typing import Any, Optional
20
+
21
+ from fastapi import FastAPI, Request
22
+ from fastapi.middleware.cors import CORSMiddleware
23
+ from fastapi.responses import JSONResponse, StreamingResponse
24
+ from pydantic import BaseModel
25
+
26
+ from .protocol import (
27
+ JsonRpcNotification,
28
+ JsonRpcRequest,
29
+ MCPErrorCode,
30
+ create_error_response,
31
+ )
32
+ from .server import MCPServer, ServerConfig, ServerState, create_server
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # ============================================================================
39
+ # Pydantic Models
40
+ # ============================================================================
41
+
42
+
43
+ class MessageRequest(BaseModel):
44
+ """Request body for JSON-RPC messages."""
45
+
46
+ jsonrpc: str = "2.0"
47
+ id: Optional[str | int] = None
48
+ method: Optional[str] = None
49
+ params: Optional[dict[str, Any]] = None
50
+ result: Optional[Any] = None
51
+ error: Optional[dict[str, Any]] = None
52
+
53
+
54
+ class HealthResponse(BaseModel):
55
+ """Health check response."""
56
+
57
+ status: str
58
+ server: dict[str, str]
59
+ timestamp: str
60
+
61
+
62
+ # ============================================================================
63
+ # SSE Manager
64
+ # ============================================================================
65
+
66
+
67
+ class SSEManager:
68
+ """
69
+ Manager for Server-Sent Events connections.
70
+
71
+ Handles multiple client connections and broadcasts messages
72
+ to all connected clients.
73
+ """
74
+
75
+ def __init__(self):
76
+ self._connections: list[asyncio.Queue] = []
77
+ self._lock = asyncio.Lock()
78
+
79
+ async def connect(self) -> asyncio.Queue:
80
+ """
81
+ Register a new SSE connection.
82
+
83
+ Returns:
84
+ Queue for receiving SSE messages
85
+ """
86
+ queue = asyncio.Queue()
87
+ async with self._lock:
88
+ self._connections.append(queue)
89
+ logger.info(f"SSE client connected. Total connections: {len(self._connections)}")
90
+ return queue
91
+
92
+ async def disconnect(self, queue: asyncio.Queue) -> None:
93
+ """Unregister an SSE connection."""
94
+ async with self._lock:
95
+ if queue in self._connections:
96
+ self._connections.remove(queue)
97
+ logger.info(f"SSE client disconnected. Total connections: {len(self._connections)}")
98
+
99
+ async def broadcast(self, message: dict[str, Any]) -> None:
100
+ """
101
+ Broadcast a message to all connected clients.
102
+
103
+ Args:
104
+ message: The message to broadcast
105
+ """
106
+ disconnected = []
107
+ async with self._lock:
108
+ for queue in self._connections:
109
+ try:
110
+ await queue.put(message)
111
+ except Exception as e:
112
+ logger.error(f"Error broadcasting to queue: {e}")
113
+ disconnected.append(queue)
114
+
115
+ # Remove disconnected clients
116
+ for queue in disconnected:
117
+ if queue in self._connections:
118
+ self._connections.remove(queue)
119
+
120
+ async def send_to_client(self, queue: asyncio.Queue, message: dict[str, Any]) -> None:
121
+ """
122
+ Send a message to a specific client.
123
+
124
+ Args:
125
+ queue: The client's queue
126
+ message: The message to send
127
+ """
128
+ try:
129
+ await queue.put(message)
130
+ except Exception as e:
131
+ logger.error(f"Error sending to client: {e}")
132
+
133
+
134
+ # ============================================================================
135
+ # HTTP Transport
136
+ # ============================================================================
137
+
138
+
139
+ class HTTPTransport:
140
+ """
141
+ HTTP transport for MCP server using FastAPI.
142
+
143
+ Provides RESTful endpoints for MCP protocol communication.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ server: Optional[MCPServer] = None,
149
+ config: Optional[ServerConfig] = None,
150
+ cors_origins: Optional[list[str]] = None,
151
+ ):
152
+ self.server = server or create_server(config=config)
153
+ self.sse_manager = SSEManager()
154
+ self.cors_origins = cors_origins or ["*"]
155
+ self.app = self._create_app()
156
+
157
+ def _create_app(self) -> FastAPI:
158
+ """Create and configure FastAPI application."""
159
+ app = FastAPI(
160
+ title="SIF MCP Server",
161
+ description="Model Context Protocol server for SIF",
162
+ version="1.0.0",
163
+ docs_url="/docs",
164
+ redoc_url="/redoc",
165
+ )
166
+
167
+ # Add CORS middleware
168
+ app.add_middleware(
169
+ CORSMiddleware,
170
+ allow_origins=self.cors_origins,
171
+ allow_credentials=True,
172
+ allow_methods=["*"],
173
+ allow_headers=["*"],
174
+ )
175
+
176
+ # Register routes
177
+ self._register_routes(app)
178
+
179
+ return app
180
+
181
+ def _register_routes(self, app: FastAPI) -> None: # noqa: C901, PLR0915
182
+ """Register FastAPI routes."""
183
+
184
+ @app.get("/health", response_model=HealthResponse)
185
+ async def health_check():
186
+ """Health check endpoint."""
187
+ return HealthResponse(
188
+ status="healthy" if self.server.state != ServerState.SHUTDOWN else "shutdown",
189
+ server=self.server.get_server_info(),
190
+ timestamp=datetime.now(timezone.utc).isoformat(),
191
+ )
192
+
193
+ @app.get("/mcp/v1/sse")
194
+ async def sse_endpoint(request: Request):
195
+ """Server-Sent Events endpoint.
196
+
197
+ Streams server-to-client messages in real-time.
198
+ """
199
+ queue = await self.sse_manager.connect()
200
+
201
+ async def event_generator() -> AsyncGenerator[str, None]:
202
+ try:
203
+ # Send initial connection message
204
+ msg = json.dumps({"message": "Connected to MCP server"})
205
+ yield f"event: connected\ndata: {msg}\n\n"
206
+
207
+ while True:
208
+ # Check if client disconnected
209
+ if await request.is_disconnected():
210
+ break
211
+
212
+ # Wait for message with timeout
213
+ try:
214
+ message = await asyncio.wait_for(queue.get(), timeout=30.0)
215
+ data = json.dumps(message)
216
+ yield f"event: message\ndata: {data}\n\n"
217
+ except asyncio.TimeoutError:
218
+ # Send keepalive
219
+ yield f"event: ping\ndata: {json.dumps({'time': datetime.now(timezone.utc).isoformat()})}\n\n"
220
+
221
+ except asyncio.CancelledError:
222
+ logger.info("SSE connection cancelled")
223
+ except Exception as e:
224
+ logger.error(f"SSE error: {e}")
225
+ finally:
226
+ await self.sse_manager.disconnect(queue)
227
+
228
+ return StreamingResponse(
229
+ event_generator(),
230
+ media_type="text/event-stream",
231
+ headers={
232
+ "Cache-Control": "no-cache",
233
+ "Connection": "keep-alive",
234
+ "X-Accel-Buffering": "no", # Disable nginx buffering
235
+ },
236
+ )
237
+
238
+ @app.post("/mcp/v1/messages")
239
+ async def messages_endpoint(request: MessageRequest):
240
+ """
241
+ JSON-RPC message endpoint.
242
+
243
+ Accepts JSON-RPC requests and returns responses.
244
+ """
245
+ # Validate JSON-RPC version
246
+ if request.jsonrpc != "2.0":
247
+ return JSONResponse(
248
+ status_code=400,
249
+ content=create_error_response(
250
+ request.id, MCPErrorCode.INVALID_REQUEST, "Invalid JSON-RPC version"
251
+ ).model_dump(),
252
+ )
253
+
254
+ # Check if it's a request or notification
255
+ if request.method is None:
256
+ return JSONResponse(
257
+ status_code=400,
258
+ content=create_error_response(
259
+ request.id, MCPErrorCode.INVALID_REQUEST, "Missing method field"
260
+ ).model_dump(),
261
+ )
262
+
263
+ if request.id is None:
264
+ # Handle notification
265
+ notification = JsonRpcNotification(
266
+ jsonrpc="2.0", method=request.method, params=request.params
267
+ )
268
+ await self.server.handle_notification(notification)
269
+ return JSONResponse(status_code=202, content={})
270
+
271
+ # Handle request
272
+ rpc_request = JsonRpcRequest(
273
+ jsonrpc="2.0", id=request.id, method=request.method, params=request.params
274
+ )
275
+ response = await self.server.handle_request(rpc_request)
276
+ return JSONResponse(content=response.model_dump(exclude_none=True))
277
+
278
+ @app.post("/mcp/v1/batch")
279
+ async def batch_endpoint(requests: list[MessageRequest]):
280
+ """
281
+ Batch JSON-RPC message endpoint.
282
+
283
+ Processes multiple JSON-RPC requests in a single call.
284
+ """
285
+ responses = []
286
+
287
+ for request in requests:
288
+ # Skip notifications in batch responses
289
+ if request.id is None:
290
+ notification = JsonRpcNotification(
291
+ jsonrpc="2.0", method=request.method, params=request.params
292
+ )
293
+ await self.server.handle_notification(notification)
294
+ else:
295
+ rpc_request = JsonRpcRequest(
296
+ jsonrpc="2.0", id=request.id, method=request.method, params=request.params
297
+ )
298
+ response = await self.server.handle_request(rpc_request)
299
+ responses.append(response.model_dump(exclude_none=True))
300
+
301
+ return JSONResponse(content=responses)
302
+
303
+ @app.get("/mcp/v1/tools")
304
+ async def list_tools():
305
+ """List available tools (non-standard endpoint for convenience)."""
306
+ tools = self.server.tool_registry.list_tools()
307
+ return JSONResponse(content={"tools": [tool.model_dump() for tool in tools]})
308
+
309
+ @app.get("/mcp/v1/status")
310
+ async def server_status():
311
+ """Get server status (non-standard endpoint for convenience)."""
312
+ status = await self.server.tool_registry.call_tool("status", {})
313
+ return JSONResponse(content=status)
314
+
315
+ async def broadcast(self, message: dict[str, Any]) -> None:
316
+ """Broadcast a message to all SSE clients."""
317
+ await self.sse_manager.broadcast(message)
318
+
319
+ async def shutdown(self) -> None:
320
+ """Shutdown the transport and server."""
321
+ await self.server.shutdown()
322
+
323
+
324
+ # ============================================================================
325
+ # FastAPI App Factory
326
+ # ============================================================================
327
+
328
+ _http_transport: Optional[HTTPTransport] = None
329
+
330
+
331
+ def create_http_app(
332
+ server: Optional[MCPServer] = None,
333
+ config: Optional[ServerConfig] = None,
334
+ cors_origins: Optional[list[str]] = None,
335
+ ) -> FastAPI:
336
+ """
337
+ Create FastAPI application for MCP HTTP transport.
338
+
339
+ Args:
340
+ server: Optional MCPServer instance
341
+ config: Optional server configuration
342
+ cors_origins: Optional CORS allowed origins
343
+
344
+ Returns:
345
+ Configured FastAPI application
346
+ """
347
+ global _http_transport
348
+ _http_transport = HTTPTransport(server=server, config=config, cors_origins=cors_origins)
349
+ return _http_transport.app
350
+
351
+
352
+ def get_http_transport() -> Optional[HTTPTransport]:
353
+ """Get the global HTTP transport instance."""
354
+ return _http_transport
355
+
356
+
357
+ # ============================================================================
358
+ # Uvicorn Runner
359
+ # ============================================================================
360
+
361
+
362
+ async def run_http_server( # noqa: PLR0913
363
+ host: str = "0.0.0.0",
364
+ port: int = 8080,
365
+ server: Optional[MCPServer] = None,
366
+ config: Optional[ServerConfig] = None,
367
+ cors_origins: Optional[list[str]] = None,
368
+ log_level: str = "info",
369
+ ):
370
+ """
371
+ Run the HTTP server using Uvicorn.
372
+
373
+ Args:
374
+ host: Host to bind to
375
+ port: Port to bind to
376
+ server: Optional MCPServer instance
377
+ config: Optional server configuration
378
+ cors_origins: Optional CORS allowed origins
379
+ log_level: Uvicorn log level
380
+ """
381
+ import uvicorn # noqa: PLC0415
382
+
383
+ app = create_http_app(server=server, config=config, cors_origins=cors_origins)
384
+
385
+ config = uvicorn.Config(app, host=host, port=port, log_level=log_level, access_log=True)
386
+
387
+ server_instance = uvicorn.Server(config)
388
+ await server_instance.serve()
389
+
390
+
391
+ # ============================================================================
392
+ # Main Entry Point
393
+ # ============================================================================
394
+
395
+
396
+ def main():
397
+ """Main entry point for HTTP transport."""
398
+ import argparse # noqa: PLC0415
399
+
400
+ parser = argparse.ArgumentParser(description="SIF MCP HTTP Server")
401
+ parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
402
+ parser.add_argument("--port", type=int, default=8080, help="Port to bind to")
403
+ parser.add_argument("--log-level", default="info", help="Log level")
404
+ parser.add_argument("--cors-origins", nargs="*", default=["*"], help="CORS allowed origins")
405
+
406
+ args = parser.parse_args()
407
+
408
+ # Setup logging
409
+ logging.basicConfig(
410
+ level=getattr(logging, args.log_level.upper()),
411
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
412
+ )
413
+
414
+ logger.info(f"Starting SIF MCP HTTP Server on {args.host}:{args.port}")
415
+
416
+ asyncio.run(
417
+ run_http_server(
418
+ host=args.host, port=args.port, cors_origins=args.cors_origins, log_level=args.log_level
419
+ )
420
+ )
421
+
422
+
423
+ if __name__ == "__main__":
424
+ main()