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,292 @@
1
+ """
2
+ MCP Stdio Transport
3
+
4
+ This module implements the stdio transport for MCP, suitable for
5
+ use with Claude Desktop and other MCP clients that communicate
6
+ via standard input/output.
7
+
8
+ The transport uses line-delimited JSON-RPC messages.
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import sys
15
+ from contextlib import asynccontextmanager
16
+ from typing import Optional, TextIO
17
+
18
+ from .protocol import JsonRpcNotification, JsonRpcRequest
19
+ from .server import MCPServer, ServerConfig, create_server
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # ============================================================================
26
+ # Stdio Transport
27
+ # ============================================================================
28
+
29
+
30
+ class StdioTransport:
31
+ """
32
+ Stdio transport for MCP server.
33
+
34
+ Reads JSON-RPC messages from stdin and writes responses to stdout.
35
+ Uses line-delimited JSON format.
36
+
37
+ Example usage:
38
+ transport = StdioTransport()
39
+ await transport.run()
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ server: Optional[MCPServer] = None,
45
+ stdin: Optional[TextIO] = None,
46
+ stdout: Optional[TextIO] = None,
47
+ encoding: str = "utf-8",
48
+ ):
49
+ self.server = server or create_server()
50
+ self.stdin = stdin or sys.stdin
51
+ self.stdout = stdout or sys.stdout
52
+ self.encoding = encoding
53
+ self._running = False
54
+ self._read_lock = asyncio.Lock()
55
+ self._write_lock = asyncio.Lock()
56
+
57
+ # -------------------------------------------------------------------------
58
+ # Message I/O
59
+ # -------------------------------------------------------------------------
60
+
61
+ async def _read_message(self) -> Optional[dict]:
62
+ """
63
+ Read a single JSON-RPC message from stdin.
64
+
65
+ Returns:
66
+ Parsed JSON object or None if EOF
67
+ """
68
+ async with self._read_lock:
69
+ # Read line from stdin (in executor to avoid blocking)
70
+ loop = asyncio.get_event_loop()
71
+ try:
72
+ line = await loop.run_in_executor(None, self.stdin.readline)
73
+ except Exception as e:
74
+ logger.error(f"Error reading from stdin: {e}")
75
+ return None
76
+
77
+ if not line:
78
+ # EOF reached
79
+ logger.info("EOF reached on stdin")
80
+ return None
81
+
82
+ line = line.strip()
83
+ if not line:
84
+ # Empty line, skip
85
+ return None
86
+
87
+ try:
88
+ message = json.loads(line)
89
+ logger.debug(f"Received: {line[:200]}...")
90
+ return message
91
+ except json.JSONDecodeError as e:
92
+ logger.error(f"Invalid JSON: {e}, line: {line[:100]}")
93
+ # Return error response
94
+ return {
95
+ "jsonrpc": "2.0",
96
+ "id": None,
97
+ "error": {"code": -32700, "message": f"Parse error: {e}"},
98
+ }
99
+
100
+ async def _write_message(self, message: dict) -> None:
101
+ """
102
+ Write a JSON-RPC message to stdout.
103
+
104
+ Args:
105
+ message: The message to write
106
+ """
107
+ async with self._write_lock:
108
+ try:
109
+ line = json.dumps(message, ensure_ascii=False)
110
+ logger.debug(f"Sending: {line[:200]}...")
111
+
112
+ # Write in executor to avoid blocking
113
+ loop = asyncio.get_event_loop()
114
+ await loop.run_in_executor(None, self._write_sync, line)
115
+
116
+ except Exception as e:
117
+ logger.error(f"Error writing to stdout: {e}")
118
+
119
+ def _write_sync(self, line: str) -> None:
120
+ """Synchronous write to stdout."""
121
+ self.stdout.write(line + "\n")
122
+ self.stdout.flush()
123
+
124
+ # -------------------------------------------------------------------------
125
+ # Message Processing
126
+ # -------------------------------------------------------------------------
127
+
128
+ async def _process_message(self, message: dict) -> Optional[dict]:
129
+ """
130
+ Process a single message.
131
+
132
+ Args:
133
+ message: The parsed JSON message
134
+
135
+ Returns:
136
+ Response dict if it's a request, None for notifications
137
+ """
138
+ # Validate JSON-RPC version
139
+ if message.get("jsonrpc") != "2.0":
140
+ return {
141
+ "jsonrpc": "2.0",
142
+ "id": message.get("id"),
143
+ "error": {"code": -32600, "message": "Invalid Request: jsonrpc must be '2.0'"},
144
+ }
145
+
146
+ # Check if it's a request or notification
147
+ msg_id = message.get("id")
148
+ method = message.get("method")
149
+
150
+ if method is None:
151
+ # This is a response (shouldn't happen in stdio transport)
152
+ logger.warning("Received response message, ignoring")
153
+ return None
154
+
155
+ if msg_id is None:
156
+ # This is a notification
157
+ notification = JsonRpcNotification(
158
+ jsonrpc="2.0", method=method, params=message.get("params")
159
+ )
160
+ await self.server.handle_notification(notification)
161
+ return None
162
+ # This is a request
163
+ request = JsonRpcRequest(
164
+ jsonrpc="2.0", id=msg_id, method=method, params=message.get("params")
165
+ )
166
+ response = await self.server.handle_request(request)
167
+ return response.model_dump(exclude_none=True)
168
+
169
+ # -------------------------------------------------------------------------
170
+ # Main Loop
171
+ # -------------------------------------------------------------------------
172
+
173
+ async def run(self) -> None:
174
+ """
175
+ Run the stdio transport main loop.
176
+
177
+ Continuously reads messages from stdin and processes them
178
+ until EOF is reached or the transport is stopped.
179
+ """
180
+ logger.info("Starting stdio transport")
181
+ self._running = True
182
+
183
+ try:
184
+ while self._running:
185
+ # Read message
186
+ message = await self._read_message()
187
+
188
+ if message is None:
189
+ # EOF or error
190
+ break
191
+
192
+ # Process message
193
+ try:
194
+ response = await self._process_message(message)
195
+
196
+ # Send response if it's a request (not notification)
197
+ if response is not None:
198
+ await self._write_message(response)
199
+
200
+ except Exception as e:
201
+ logger.exception("Error processing message")
202
+ # Send error response if we have an ID
203
+ msg_id = message.get("id")
204
+ if msg_id is not None:
205
+ error_response = {
206
+ "jsonrpc": "2.0",
207
+ "id": msg_id,
208
+ "error": {"code": -32603, "message": f"Internal error: {e!s}"},
209
+ }
210
+ await self._write_message(error_response)
211
+
212
+ except asyncio.CancelledError:
213
+ logger.info("Transport cancelled")
214
+ raise
215
+
216
+ finally:
217
+ self._running = False
218
+ logger.info("Stdio transport stopped")
219
+
220
+ def stop(self) -> None:
221
+ """Stop the transport."""
222
+ logger.info("Stopping stdio transport")
223
+ self._running = False
224
+
225
+ async def shutdown(self) -> None:
226
+ """Shutdown the transport and server."""
227
+ self.stop()
228
+ await self.server.shutdown()
229
+
230
+
231
+ # ============================================================================
232
+ # Context Manager
233
+ # ============================================================================
234
+
235
+
236
+ @asynccontextmanager
237
+ async def stdio_transport(
238
+ server: Optional[MCPServer] = None, config: Optional[ServerConfig] = None
239
+ ):
240
+ """
241
+ Async context manager for stdio transport.
242
+
243
+ Example:
244
+ async with stdio_transport() as transport:
245
+ await transport.run()
246
+
247
+ Args:
248
+ server: Optional MCPServer instance
249
+ config: Optional server configuration
250
+
251
+ Yields:
252
+ StdioTransport instance
253
+ """
254
+ if server is None:
255
+ server = create_server(config=config)
256
+
257
+ transport = StdioTransport(server=server)
258
+ try:
259
+ yield transport
260
+ finally:
261
+ await transport.shutdown()
262
+
263
+
264
+ # ============================================================================
265
+ # Main Entry Point
266
+ # ============================================================================
267
+
268
+
269
+ async def main():
270
+ """Main entry point for stdio transport."""
271
+ # Setup logging to stderr (don't pollute stdout)
272
+ logging.basicConfig(
273
+ level=logging.INFO,
274
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
275
+ stream=sys.stderr,
276
+ )
277
+
278
+ logger.info("Starting SIF MCP Server (stdio mode)")
279
+
280
+ async with stdio_transport() as transport:
281
+ await transport.run()
282
+
283
+ logger.info("SIF MCP Server stopped")
284
+
285
+
286
+ def run_stdio():
287
+ """Run the stdio transport (synchronous entry point)."""
288
+ asyncio.run(main())
289
+
290
+
291
+ if __name__ == "__main__":
292
+ run_stdio()
@@ -0,0 +1,26 @@
1
+ """MCP (Model Context Protocol) server implementation for SIF.
2
+
3
+ This package provides MCP server functionality:
4
+ - stdio transport for CLI integration
5
+ - HTTP/SSE transport for web integration
6
+ - MCP protocol handlers
7
+ - Tool and resource definitions
8
+ """
9
+
10
+ from sif.mcp_server.handlers import ResourceHandler, ToolHandler
11
+ from sif.mcp_server.server import MCPServer
12
+ from sif.mcp_server.tools import CollectionTool, IndexTool, SearchTool
13
+ from sif.mcp_server.transport import HTTPTransport, StdioTransport, Transport
14
+
15
+
16
+ __all__ = [
17
+ "CollectionTool",
18
+ "HTTPTransport",
19
+ "IndexTool",
20
+ "MCPServer",
21
+ "ResourceHandler",
22
+ "SearchTool",
23
+ "StdioTransport",
24
+ "ToolHandler",
25
+ "Transport",
26
+ ]
@@ -0,0 +1,68 @@
1
+ """MCP request handlers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from sif.utils.logging import get_logger
7
+
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class ToolHandler(ABC):
13
+ """Abstract base class for MCP tool handlers."""
14
+
15
+ @property
16
+ @abstractmethod
17
+ def name(self) -> str:
18
+ """Tool name."""
19
+ ...
20
+
21
+ @property
22
+ @abstractmethod
23
+ def description(self) -> str:
24
+ """Tool description."""
25
+ ...
26
+
27
+ @property
28
+ @abstractmethod
29
+ def input_schema(self) -> dict[str, Any]:
30
+ """JSON schema for tool input."""
31
+ ...
32
+
33
+ @abstractmethod
34
+ def handle(self, params: dict[str, Any]) -> dict[str, Any]:
35
+ """Handle a tool call.
36
+
37
+ Args:
38
+ params: Tool parameters
39
+
40
+ Returns:
41
+ Tool result
42
+ """
43
+ ...
44
+
45
+
46
+ class ResourceHandler(ABC):
47
+ """Abstract base class for MCP resource handlers."""
48
+
49
+ @property
50
+ @abstractmethod
51
+ def uri(self) -> str:
52
+ """Resource URI."""
53
+ ...
54
+
55
+ @property
56
+ @abstractmethod
57
+ def mime_type(self) -> str:
58
+ """Resource MIME type."""
59
+ ...
60
+
61
+ @abstractmethod
62
+ def handle(self) -> dict[str, Any]:
63
+ """Handle a resource request.
64
+
65
+ Returns:
66
+ Resource content
67
+ """
68
+ ...
@@ -0,0 +1,58 @@
1
+ """MCP server implementation."""
2
+
3
+ from sif.mcp_server.transport import Transport
4
+ from sif.utils.logging import get_logger
5
+
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ class MCPServer:
11
+ """Model Context Protocol server for SIF.
12
+
13
+ Provides MCP-compliant endpoints for integration with
14
+ AI assistants and other MCP-compatible tools.
15
+
16
+ Supports both stdio and HTTP transports.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ transport: Transport,
22
+ # tool_handlers: list[ToolHandler] | None = None,
23
+ # resource_handlers: list[ResourceHandler] | None = None,
24
+ ) -> None:
25
+ """Initialize MCP server.
26
+
27
+ Args:
28
+ transport: Transport layer (stdio or HTTP)
29
+ tool_handlers: Tool request handlers
30
+ resource_handlers: Resource request handlers
31
+ """
32
+ self._transport = transport
33
+ # self._tool_handlers = tool_handlers or []
34
+ # self._resource_handlers = resource_handlers or []
35
+ self._running = False
36
+
37
+ def start(self) -> None:
38
+ """Start the MCP server."""
39
+ logger.info("Starting MCP server")
40
+ self._running = True
41
+
42
+ try:
43
+ self._transport.start()
44
+ except KeyboardInterrupt:
45
+ logger.info("Received interrupt signal")
46
+ finally:
47
+ self.stop()
48
+
49
+ def stop(self) -> None:
50
+ """Stop the MCP server."""
51
+ logger.info("Stopping MCP server")
52
+ self._running = False
53
+ self._transport.stop()
54
+
55
+ @property
56
+ def is_running(self) -> bool:
57
+ """Check if server is running."""
58
+ return self._running
@@ -0,0 +1,216 @@
1
+ """MCP tool implementations."""
2
+
3
+ from typing import Any
4
+
5
+ from sif.mcp_server.handlers import ToolHandler
6
+ from sif.utils.logging import get_logger
7
+
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class SearchTool(ToolHandler):
13
+ """MCP tool for searching documents."""
14
+
15
+ def __init__(self, search_service: "SearchService") -> None: # noqa: F821
16
+ """Initialize search tool.
17
+
18
+ Args:
19
+ search_service: Search service instance
20
+ """
21
+ self._search_service = search_service
22
+
23
+ @property
24
+ def name(self) -> str:
25
+ return "search"
26
+
27
+ @property
28
+ def description(self) -> str:
29
+ return "Search indexed documents using full-text and semantic search"
30
+
31
+ @property
32
+ def input_schema(self) -> dict[str, Any]:
33
+ return {
34
+ "type": "object",
35
+ "properties": {
36
+ "query": {
37
+ "type": "string",
38
+ "description": "Search query",
39
+ },
40
+ "collection": {
41
+ "type": "string",
42
+ "description": "Collection to search (optional)",
43
+ },
44
+ "limit": {
45
+ "type": "integer",
46
+ "description": "Maximum number of results",
47
+ "default": 10,
48
+ },
49
+ "search_type": {
50
+ "type": "string",
51
+ "enum": ["bm25", "vector", "hybrid"],
52
+ "description": "Search type",
53
+ "default": "hybrid",
54
+ },
55
+ },
56
+ "required": ["query"],
57
+ }
58
+
59
+ def handle(self, params: dict[str, Any]) -> dict[str, Any]:
60
+ """Handle search tool call."""
61
+ query = params.get("query", "")
62
+ collection = params.get("collection")
63
+ limit = params.get("limit", 10)
64
+ search_type = params.get("search_type", "hybrid")
65
+
66
+ logger.info(f"Search tool called: query='{query}', type='{search_type}'")
67
+
68
+ # Placeholder: actual implementation would call search service
69
+ return {
70
+ "content": [
71
+ {
72
+ "type": "text",
73
+ "text": f"Search results for '{query}':\n\n1. Result 1\n2. Result 2",
74
+ },
75
+ ],
76
+ }
77
+
78
+
79
+ class IndexTool(ToolHandler):
80
+ """MCP tool for indexing documents."""
81
+
82
+ def __init__(self, indexer: "DocumentIndexer") -> None: # noqa: F821
83
+ """Initialize index tool.
84
+
85
+ Args:
86
+ indexer: Document indexer instance
87
+ """
88
+ self._indexer = indexer
89
+
90
+ @property
91
+ def name(self) -> str:
92
+ return "index"
93
+
94
+ @property
95
+ def description(self) -> str:
96
+ return "Index documents in a collection"
97
+
98
+ @property
99
+ def input_schema(self) -> dict[str, Any]:
100
+ return {
101
+ "type": "object",
102
+ "properties": {
103
+ "collection": {
104
+ "type": "string",
105
+ "description": "Collection name",
106
+ },
107
+ "path": {
108
+ "type": "string",
109
+ "description": "Path to index",
110
+ },
111
+ },
112
+ "required": ["collection"],
113
+ }
114
+
115
+ def handle(self, params: dict[str, Any]) -> dict[str, Any]:
116
+ """Handle index tool call."""
117
+ collection = params.get("collection", "")
118
+ path = params.get("path")
119
+
120
+ logger.info(f"Index tool called: collection='{collection}'")
121
+
122
+ return {
123
+ "content": [
124
+ {
125
+ "type": "text",
126
+ "text": f"Indexing collection '{collection}'...",
127
+ },
128
+ ],
129
+ }
130
+
131
+
132
+ class CollectionTool(ToolHandler):
133
+ """MCP tool for managing collections."""
134
+
135
+ def __init__(self, collection_manager: "CollectionManager") -> None: # noqa: F821
136
+ """Initialize collection tool.
137
+
138
+ Args:
139
+ collection_manager: Collection manager instance
140
+ """
141
+ self._collection_manager = collection_manager
142
+
143
+ @property
144
+ def name(self) -> str:
145
+ return "collection"
146
+
147
+ @property
148
+ def description(self) -> str:
149
+ return "List, create, or manage document collections"
150
+
151
+ @property
152
+ def input_schema(self) -> dict[str, Any]:
153
+ return {
154
+ "type": "object",
155
+ "properties": {
156
+ "action": {
157
+ "type": "string",
158
+ "enum": ["list", "create", "delete"],
159
+ "description": "Action to perform",
160
+ },
161
+ "name": {
162
+ "type": "string",
163
+ "description": "Collection name (for create/delete)",
164
+ },
165
+ "path": {
166
+ "type": "string",
167
+ "description": "Path to add (for create)",
168
+ },
169
+ },
170
+ "required": ["action"],
171
+ }
172
+
173
+ def handle(self, params: dict[str, Any]) -> dict[str, Any]:
174
+ """Handle collection tool call."""
175
+ action = params.get("action", "list")
176
+ name = params.get("name")
177
+ path = params.get("path")
178
+
179
+ logger.info(f"Collection tool called: action='{action}'")
180
+
181
+ if action == "list":
182
+ return {
183
+ "content": [
184
+ {
185
+ "type": "text",
186
+ "text": "Collections:\n\n1. my-notes\n2. work-docs",
187
+ },
188
+ ],
189
+ }
190
+ if action == "create":
191
+ return {
192
+ "content": [
193
+ {
194
+ "type": "text",
195
+ "text": f"Created collection '{name}'",
196
+ },
197
+ ],
198
+ }
199
+ if action == "delete":
200
+ return {
201
+ "content": [
202
+ {
203
+ "type": "text",
204
+ "text": f"Deleted collection '{name}'",
205
+ },
206
+ ],
207
+ }
208
+
209
+ return {
210
+ "content": [
211
+ {
212
+ "type": "text",
213
+ "text": f"Unknown action: {action}",
214
+ },
215
+ ],
216
+ }