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.
- docsif-0.1.0.dist-info/METADATA +393 -0
- docsif-0.1.0.dist-info/RECORD +86 -0
- docsif-0.1.0.dist-info/WHEEL +4 -0
- docsif-0.1.0.dist-info/entry_points.txt +3 -0
- docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
- sif/__init__.py +24 -0
- sif/_version.py +3 -0
- sif/cli/__init__.py +11 -0
- sif/cli/commands/__init__.py +18 -0
- sif/cli/commands/bench.py +148 -0
- sif/cli/commands/collection.py +360 -0
- sif/cli/commands/context.py +154 -0
- sif/cli/commands/get.py +200 -0
- sif/cli/commands/index.py +294 -0
- sif/cli/commands/ls.py +67 -0
- sif/cli/commands/mcp.py +93 -0
- sif/cli/commands/pull.py +111 -0
- sif/cli/commands/search.py +574 -0
- sif/cli/config.py +206 -0
- sif/cli/formatters.py +269 -0
- sif/cli/main.py +205 -0
- sif/config/__init__.py +30 -0
- sif/config/constants.py +52 -0
- sif/config/settings.py +217 -0
- sif/core/__init__.py +21 -0
- sif/core/collection.py +161 -0
- sif/core/context.py +167 -0
- sif/core/document.py +125 -0
- sif/core/models.py +298 -0
- sif/database/__init__.py +23 -0
- sif/database/connection.py +139 -0
- sif/database/database.py +136 -0
- sif/database/migrations.py +187 -0
- sif/database/repositories.py +500 -0
- sif/database/repository.py +171 -0
- sif/database/schema.py +362 -0
- sif/embedding/__init__.py +22 -0
- sif/embedding/cache.py +196 -0
- sif/embedding/embedder.py +328 -0
- sif/embedding/factory.py +92 -0
- sif/embedding/manager.py +200 -0
- sif/embedding/model.py +126 -0
- sif/indexing/__init__.py +21 -0
- sif/indexing/chunker.py +226 -0
- sif/indexing/indexer.py +226 -0
- sif/indexing/parser.py +192 -0
- sif/indexing/scanner.py +148 -0
- sif/indexing/watcher.py +156 -0
- sif/mcp/README.md +258 -0
- sif/mcp/__init__.py +9 -0
- sif/mcp/claude_desktop_config.json +16 -0
- sif/mcp/cli.py +92 -0
- sif/mcp/protocol.py +336 -0
- sif/mcp/server.py +433 -0
- sif/mcp/server_http.py +54 -0
- sif/mcp/test_mcp.py +108 -0
- sif/mcp/tools.py +497 -0
- sif/mcp/transport_http.py +424 -0
- sif/mcp/transport_stdio.py +292 -0
- sif/mcp_server/__init__.py +26 -0
- sif/mcp_server/handlers.py +68 -0
- sif/mcp_server/server.py +58 -0
- sif/mcp_server/tools.py +216 -0
- sif/mcp_server/transport.py +221 -0
- sif/models/__init__.py +70 -0
- sif/models/collection.py +51 -0
- sif/models/context.py +45 -0
- sif/models/document.py +76 -0
- sif/models/download.py +208 -0
- sif/models/embedding.py +71 -0
- sif/models/search.py +92 -0
- sif/search/__init__.py +22 -0
- sif/search/benchmark.py +110 -0
- sif/search/bm25.py +216 -0
- sif/search/expansion.py +157 -0
- sif/search/hybrid.py +323 -0
- sif/search/rerank.py +390 -0
- sif/search/rrf.py +186 -0
- sif/search/snippets.py +130 -0
- sif/search/strategy.py +97 -0
- sif/search/vector.py +177 -0
- sif/utils/__init__.py +26 -0
- sif/utils/logging.py +143 -0
- sif/utils/paths.py +82 -0
- sif/utils/progress.py +83 -0
- 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
|
+
...
|
sif/mcp_server/server.py
ADDED
|
@@ -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
|
sif/mcp_server/tools.py
ADDED
|
@@ -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
|
+
}
|