ims-mcp 1.0.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.
ims_mcp/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """IMS MCP Server - Model Context Protocol server for IMS (Instruction Management Systems).
2
+
3
+ This package provides a FastMCP server that connects to IMS
4
+ for advanced retrieval-augmented generation capabilities.
5
+
6
+ Environment Variables:
7
+ R2R_API_BASE: IMS server URL (default: http://localhost:7272)
8
+ R2R_COLLECTION: Collection name for queries (optional)
9
+ R2R_API_KEY: API key for authentication (optional)
10
+
11
+ Note: Environment variables use R2R_ prefix for compatibility with underlying R2R SDK.
12
+ """
13
+
14
+ __version__ = "1.0.0"
15
+ __author__ = "Igor Solomatov"
16
+
17
+ from ims_mcp.server import mcp
18
+
19
+ __all__ = ["mcp", "__version__"]
20
+
ims_mcp/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Entry point for running ims-mcp as a module.
2
+
3
+ This allows the package to be executed as:
4
+ python -m ims_mcp
5
+ """
6
+
7
+ from ims_mcp.server import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
11
+
ims_mcp/server.py ADDED
@@ -0,0 +1,512 @@
1
+ """R2R MCP Server - FastMCP server for R2R retrieval system.
2
+
3
+ This module provides a Model Context Protocol (MCP) server that connects to R2R
4
+ for advanced retrieval-augmented generation capabilities.
5
+
6
+ Environment Variables:
7
+ R2R_API_BASE or R2R_BASE_URL: R2R server URL (default: http://localhost:7272)
8
+ R2R_COLLECTION: Collection name for queries (optional, uses server default)
9
+ R2R_API_KEY: API key for authentication (optional)
10
+
11
+ The R2RClient automatically reads these environment variables, so no manual
12
+ configuration is needed when running via uvx or other launchers.
13
+ """
14
+
15
+ import uuid
16
+ from r2r import R2RClient
17
+
18
+
19
+ def id_to_shorthand(id: str) -> str:
20
+ """Convert a full ID to shortened version for display."""
21
+ return str(id)[:7]
22
+
23
+
24
+ def format_search_results_for_llm(results) -> str:
25
+ """
26
+ Format R2R search results for LLM consumption.
27
+
28
+ Formats vector search, graph search, web search, and document search results
29
+ into a readable text format with source IDs and relevant metadata.
30
+ """
31
+ lines = []
32
+
33
+ # 1) Chunk search
34
+ if results.chunk_search_results:
35
+ lines.append("Vector Search Results:")
36
+ for c in results.chunk_search_results:
37
+ lines.append(f"Source ID [{id_to_shorthand(c.id)}]:")
38
+ lines.append(c.text or "") # or c.text[:200] to truncate
39
+
40
+ # 2) Graph search
41
+ if results.graph_search_results:
42
+ lines.append("Graph Search Results:")
43
+ for g in results.graph_search_results:
44
+ lines.append(f"Source ID [{id_to_shorthand(g.id)}]:")
45
+ if hasattr(g.content, "summary"):
46
+ lines.append(f"Community Name: {g.content.name}")
47
+ lines.append(f"ID: {g.content.id}")
48
+ lines.append(f"Summary: {g.content.summary}")
49
+ elif hasattr(g.content, "name") and hasattr(
50
+ g.content, "description"
51
+ ):
52
+ lines.append(f"Entity Name: {g.content.name}")
53
+ lines.append(f"Description: {g.content.description}")
54
+ elif (
55
+ hasattr(g.content, "subject")
56
+ and hasattr(g.content, "predicate")
57
+ and hasattr(g.content, "object")
58
+ ):
59
+ lines.append(
60
+ f"Relationship: {g.content.subject}-{g.content.predicate}-{g.content.object}"
61
+ )
62
+
63
+ # 3) Web search
64
+ if results.web_search_results:
65
+ lines.append("Web Search Results:")
66
+ for w in results.web_search_results:
67
+ lines.append(f"Source ID [{id_to_shorthand(w.id)}]:")
68
+ lines.append(f"Title: {w.title}")
69
+ lines.append(f"Link: {w.link}")
70
+ lines.append(f"Snippet: {w.snippet}")
71
+
72
+ # 4) Local context docs
73
+ if results.document_search_results:
74
+ lines.append("Local Context Documents:")
75
+ for doc_result in results.document_search_results:
76
+ doc_title = doc_result.title or "Untitled Document"
77
+ doc_id = doc_result.id
78
+ summary = doc_result.summary
79
+
80
+ lines.append(f"Full Document ID: {doc_id}")
81
+ lines.append(f"Shortened Document ID: {id_to_shorthand(doc_id)}")
82
+ lines.append(f"Document Title: {doc_title}")
83
+ if summary:
84
+ lines.append(f"Summary: {summary}")
85
+
86
+ if doc_result.chunks:
87
+ # Then each chunk inside:
88
+ for chunk in doc_result.chunks:
89
+ lines.append(
90
+ f"\nChunk ID {id_to_shorthand(chunk['id'])}:\n{chunk['text']}"
91
+ )
92
+
93
+ result = "\n".join(lines)
94
+ return result
95
+
96
+
97
+ # Create a FastMCP server
98
+ try:
99
+ from mcp.server.fastmcp import FastMCP
100
+
101
+ mcp = FastMCP("R2R Retrieval System")
102
+ except Exception as e:
103
+ raise ImportError(
104
+ "MCP is not installed. Please run `pip install mcp`"
105
+ ) from e
106
+
107
+
108
+ # Search tool with filtering support
109
+ @mcp.tool()
110
+ async def search(
111
+ query: str,
112
+ filters: dict | None = None,
113
+ limit: float | None = None, # Use float to accept JSON "number" type, convert to int internally
114
+ use_semantic_search: bool | None = None,
115
+ use_fulltext_search: bool | None = None,
116
+ ) -> str:
117
+ """
118
+ Performs a search with optional filtering and configuration
119
+
120
+ Args:
121
+ query: The search query
122
+ filters: Metadata filters (e.g., {"tags": {"$in": ["agents"]}})
123
+ limit: Maximum number of results (server default if not specified)
124
+ use_semantic_search: Enable semantic search (server default if not specified)
125
+ use_fulltext_search: Enable fulltext search (server default if not specified)
126
+
127
+ Returns:
128
+ Formatted search results from the knowledge base
129
+ """
130
+ client = R2RClient()
131
+
132
+ # Only build search_settings if user provided any parameters
133
+ # This preserves original behavior: search("query") → search(query=query) with NO search_settings
134
+ kwargs = {"query": query}
135
+
136
+ if any(
137
+ param is not None
138
+ for param in [filters, limit, use_semantic_search, use_fulltext_search]
139
+ ):
140
+ search_settings = {}
141
+
142
+ if filters is not None:
143
+ search_settings["filters"] = filters
144
+ if limit is not None:
145
+ search_settings["limit"] = int(limit) # Convert to int for R2R API
146
+ if use_semantic_search is not None:
147
+ search_settings["use_semantic_search"] = use_semantic_search
148
+ if use_fulltext_search is not None:
149
+ search_settings["use_fulltext_search"] = use_fulltext_search
150
+
151
+ kwargs["search_settings"] = search_settings
152
+
153
+ search_response = client.retrieval.search(**kwargs)
154
+ return format_search_results_for_llm(search_response.results)
155
+
156
+
157
+ # RAG query tool with filtering and generation config
158
+ @mcp.tool()
159
+ async def rag(
160
+ query: str,
161
+ filters: dict | None = None,
162
+ limit: float | None = None, # Use float to accept JSON "number" type, convert to int internally
163
+ model: str | None = None,
164
+ temperature: float | None = None,
165
+ max_tokens: float | None = None, # Use float to accept JSON "number" type, convert to int internally
166
+ ) -> str:
167
+ """
168
+ Perform RAG query with optional filtering and generation config
169
+
170
+ Args:
171
+ query: The question to answer
172
+ filters: Metadata filters (e.g., {"tags": {"$in": ["agents"]}})
173
+ limit: Max search results to use (server default if not specified)
174
+ model: LLM model to use (server default if not specified)
175
+ temperature: Response randomness 0-1 (server default if not specified)
176
+ max_tokens: Max response length (server default if not specified)
177
+
178
+ Returns:
179
+ Generated answer from RAG
180
+ """
181
+ client = R2RClient()
182
+
183
+ # Only build configs if user provided parameters
184
+ # This preserves original behavior: rag("query") → rag(query=query) with NO configs
185
+ kwargs = {"query": query}
186
+
187
+ # Build search_settings if any search params provided
188
+ if any(param is not None for param in [filters, limit]):
189
+ search_settings = {}
190
+ if filters is not None:
191
+ search_settings["filters"] = filters
192
+ if limit is not None:
193
+ search_settings["limit"] = int(limit) # Convert to int for R2R API
194
+ kwargs["search_settings"] = search_settings
195
+
196
+ # Build rag_generation_config if any generation params provided
197
+ if any(param is not None for param in [model, temperature, max_tokens]):
198
+ rag_config = {}
199
+ if model is not None:
200
+ rag_config["model"] = model
201
+ if temperature is not None:
202
+ rag_config["temperature"] = temperature
203
+ if max_tokens is not None:
204
+ rag_config["max_tokens"] = int(max_tokens) # Convert to int for R2R API
205
+ kwargs["rag_generation_config"] = rag_config
206
+
207
+ rag_response = client.retrieval.rag(**kwargs)
208
+ return rag_response.results.generated_answer # type: ignore
209
+
210
+
211
+ # Document upload tool with upsert semantics
212
+ @mcp.tool()
213
+ async def put_document(
214
+ content: str,
215
+ title: str,
216
+ metadata: dict | None = None,
217
+ document_id: str | None = None,
218
+ ) -> str:
219
+ """
220
+ Upload or update a document with upsert semantics
221
+
222
+ Args:
223
+ content: The text content of the document
224
+ title: Document title (used for ID generation if document_id not provided)
225
+ metadata: Additional metadata (e.g., {"tags": ["agents"], "domain": "dev"})
226
+ document_id: Optional document ID for explicit upsert
227
+
228
+ Returns:
229
+ Status message with document_id and operation type
230
+ """
231
+ client = R2RClient()
232
+
233
+ # Build metadata with title
234
+ final_metadata = {"title": title}
235
+ if metadata:
236
+ final_metadata.update(metadata)
237
+
238
+ # Generate UUID from title if not provided (enables upsert by title)
239
+ if not document_id:
240
+ # Use UUID5 to create deterministic UUID from title
241
+ # Same title always generates same UUID for upsert semantics
242
+ namespace = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8") # DNS namespace
243
+ document_id = str(uuid.uuid5(namespace, title))
244
+
245
+ # Try to create, if exists then update
246
+ try:
247
+ result = client.documents.create(
248
+ raw_text=content,
249
+ id=document_id,
250
+ metadata=final_metadata,
251
+ run_with_orchestration=True,
252
+ )
253
+ return f"Document created successfully.\nDocument ID: {document_id}\nOperation: CREATE"
254
+ except Exception as e:
255
+ error_msg = str(e)
256
+ # Check if document already exists
257
+ if "already exists" in error_msg.lower():
258
+ # Document exists, delete and recreate for true upsert
259
+ try:
260
+ # Delete existing document
261
+ client.documents.delete(id=document_id)
262
+
263
+ # Recreate with new content
264
+ result = client.documents.create(
265
+ raw_text=content,
266
+ id=document_id,
267
+ metadata=final_metadata,
268
+ run_with_orchestration=True,
269
+ )
270
+ return f"Document updated successfully.\nDocument ID: {document_id}\nOperation: UPDATE (delete + recreate)"
271
+ except Exception as update_error:
272
+ return f"Error updating document: {str(update_error)}"
273
+ else:
274
+ # Different error, re-raise
275
+ return f"Error creating document: {error_msg}"
276
+
277
+
278
+ # List documents tool
279
+ @mcp.tool()
280
+ async def list_documents(
281
+ offset: float = 0, # Use float to accept JSON "number" type, convert to int internally
282
+ limit: float = 100, # Use float to accept JSON "number" type, convert to int internally
283
+ document_ids: list[str] | None = None,
284
+ compact_view: bool = False,
285
+ tags: list[str] | None = None,
286
+ match_all_tags: bool = False,
287
+ ) -> str:
288
+ """
289
+ List documents in the R2R knowledge base with pagination
290
+
291
+ Args:
292
+ offset: Number of documents to skip (default: 0)
293
+ limit: Maximum number of documents to return (default: 100, max: 100)
294
+ document_ids: Optional list of specific document IDs to retrieve
295
+ compact_view: Show only ID and title (default: False - shows all details)
296
+ tags: Optional list of tags to filter by (e.g., ["agents", "r1"])
297
+ match_all_tags: If True, document must have ALL tags; if False (default), document must have ANY tag
298
+
299
+ Returns:
300
+ Formatted list of documents
301
+ """
302
+ client = R2RClient()
303
+
304
+ # Build kwargs for list call
305
+ kwargs = {"offset": int(offset), "limit": min(int(limit), 100)} # Convert to int, cap at 100
306
+
307
+ if document_ids:
308
+ kwargs["ids"] = document_ids
309
+
310
+ # List documents
311
+ result = client.documents.list(**kwargs)
312
+
313
+ # Filter by tags if provided
314
+ filtered_results = result.results
315
+ if tags and len(tags) > 0:
316
+ provided_tags = set(tags)
317
+ filtered_results = []
318
+
319
+ for doc in result.results:
320
+ # Extract tags from document metadata
321
+ doc_tags = set()
322
+ if hasattr(doc, 'metadata') and doc.metadata:
323
+ tags_value = doc.metadata.get('tags')
324
+ if tags_value:
325
+ if isinstance(tags_value, list):
326
+ doc_tags = set(tags_value)
327
+ elif isinstance(tags_value, str):
328
+ doc_tags = {tags_value}
329
+
330
+ # Apply filter based on match mode
331
+ if match_all_tags:
332
+ # ALL mode: document must have all provided tags
333
+ if provided_tags.issubset(doc_tags):
334
+ filtered_results.append(doc)
335
+ else:
336
+ # ANY mode: document must have at least one provided tag
337
+ if len(provided_tags.intersection(doc_tags)) > 0:
338
+ filtered_results.append(doc)
339
+
340
+ # Format results for display
341
+ lines = []
342
+ if tags:
343
+ tag_mode = "ALL" if match_all_tags else "ANY"
344
+ lines.append(f"Documents with {tag_mode} tags {tags} (showing {len(filtered_results)} of {result.total_entries} total):\n")
345
+ else:
346
+ lines.append(f"Documents (showing {len(filtered_results)} of {result.total_entries} total):\n")
347
+
348
+ for doc in filtered_results:
349
+ if compact_view:
350
+ # Compact mode: just ID and title
351
+ lines.append(f"ID: {doc.id} | Title: {doc.title or 'Untitled'}")
352
+ else:
353
+ # Full mode: all details
354
+ lines.append(f"{'='*60}")
355
+ lines.append(f"ID: {doc.id}")
356
+ lines.append(f"Title: {doc.title or 'Untitled'}")
357
+
358
+ # Show all metadata
359
+ if hasattr(doc, 'metadata') and doc.metadata:
360
+ lines.append("Metadata:")
361
+ for key, value in doc.metadata.items():
362
+ # Format value based on type
363
+ if isinstance(value, list):
364
+ lines.append(f" {key}: {', '.join(str(v) for v in value)}")
365
+ elif isinstance(value, dict):
366
+ lines.append(f" {key}: {value}")
367
+ else:
368
+ lines.append(f" {key}: {value}")
369
+
370
+ lines.append(f"Status: {doc.ingestion_status}")
371
+ lines.append(f"Size: {doc.size_in_bytes} bytes")
372
+ lines.append(f"Created: {doc.created_at}")
373
+ lines.append(f"Updated: {doc.updated_at}")
374
+
375
+ if hasattr(doc, 'summary') and doc.summary:
376
+ lines.append(f"Summary: {doc.summary[:200]}...")
377
+
378
+ return "\n".join(lines)
379
+
380
+
381
+ # Get document tool
382
+ @mcp.tool()
383
+ async def get_document(
384
+ document_id: str | None = None,
385
+ title: str | None = None,
386
+ ) -> str:
387
+ """
388
+ Retrieve a document by ID or title
389
+
390
+ Args:
391
+ document_id: Document ID to retrieve
392
+ title: Document title to search for (if document_id not provided)
393
+
394
+ Returns:
395
+ Formatted document details
396
+ """
397
+ client = R2RClient()
398
+
399
+ # Validate that at least one parameter is provided
400
+ if not document_id and not title:
401
+ return "Error: Must provide either document_id or title"
402
+
403
+ # If only title provided, search for document by title using metadata filter
404
+ if not document_id and title:
405
+ try:
406
+ # Use search API with title filter for more efficient lookup
407
+ search_result = client.retrieval.search(
408
+ query=title,
409
+ search_settings={
410
+ "filters": {"title": {"$eq": title}},
411
+ "limit": 5,
412
+ "use_fulltext_search": True, # Use fulltext for exact match
413
+ }
414
+ )
415
+
416
+ # Extract document IDs from search results
417
+ matching_docs = []
418
+ if hasattr(search_result, 'results') and hasattr(search_result.results, 'chunk_search_results'):
419
+ seen_doc_ids = set()
420
+ for chunk in search_result.results.chunk_search_results:
421
+ if hasattr(chunk, 'metadata') and chunk.metadata:
422
+ doc_id = chunk.metadata.get('document_id')
423
+ doc_title = chunk.metadata.get('title')
424
+ if doc_id and doc_id not in seen_doc_ids:
425
+ seen_doc_ids.add(doc_id)
426
+ matching_docs.append((doc_id, doc_title))
427
+
428
+ # Fallback: Use list API if search didn't work
429
+ if not matching_docs:
430
+ list_result = client.documents.list(limit=100)
431
+ for doc in list_result.results:
432
+ if doc.title and doc.title.lower() == title.lower():
433
+ matching_docs.append((doc.id, doc.title))
434
+
435
+ if not matching_docs:
436
+ return f"Error: No document found with title '{title}'"
437
+
438
+ if len(matching_docs) > 1:
439
+ lines = [f"Warning: Found {len(matching_docs)} documents with title '{title}':"]
440
+ for doc_id, doc_title in matching_docs:
441
+ lines.append(f" - ID: {doc_id}")
442
+ lines.append("\nPlease use document_id to retrieve a specific document.")
443
+ return "\n".join(lines)
444
+
445
+ # Found exactly one match
446
+ document_id = matching_docs[0][0]
447
+ except Exception as e:
448
+ return f"Error searching for document by title: {str(e)}"
449
+
450
+ # Now we have document_id, download the original file
451
+ try:
452
+ # Download the original document content
453
+ file_content = client.documents.download(id=document_id)
454
+
455
+ # Read and decode the content
456
+ content_bytes = file_content.read()
457
+ content_text = content_bytes.decode('utf-8')
458
+
459
+ # Build output with just document ID and content
460
+ output_lines = [f"DOCUMENT ID: {document_id}", content_text]
461
+
462
+ return "\n".join(output_lines)
463
+
464
+ except Exception as e:
465
+ error_msg = str(e)
466
+ if "not found" in error_msg.lower():
467
+ return f"Error: Document with ID '{document_id}' not found"
468
+ return f"Error downloading document: {error_msg}"
469
+
470
+
471
+ # Delete document tool
472
+ @mcp.tool()
473
+ async def delete_document(document_id: str) -> str:
474
+ """
475
+ Delete a document by ID
476
+
477
+ Args:
478
+ document_id: The unique identifier of the document to delete
479
+
480
+ Returns:
481
+ Status message confirming deletion or describing error
482
+ """
483
+ client = R2RClient()
484
+
485
+ try:
486
+ # Delete the document
487
+ client.documents.delete(id=document_id)
488
+ return f"Document deleted successfully.\nDocument ID: {document_id}"
489
+ except Exception as e:
490
+ error_msg = str(e)
491
+
492
+ # Handle common error cases with user-friendly messages
493
+ if "not found" in error_msg.lower():
494
+ return f"Error: Document with ID '{document_id}' not found"
495
+ elif "permission" in error_msg.lower() or "denied" in error_msg.lower():
496
+ return f"Error: Permission denied to delete document '{document_id}'"
497
+ elif "connection" in error_msg.lower() or "timeout" in error_msg.lower():
498
+ return f"Error: Unable to communicate with R2R server. Please check connection."
499
+ else:
500
+ # Generic error fallback
501
+ return f"Error deleting document: {error_msg}"
502
+
503
+
504
+ def main():
505
+ """Main entry point for console script."""
506
+ mcp.run()
507
+
508
+
509
+ # Run the server if executed directly
510
+ if __name__ == "__main__":
511
+ main()
512
+
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.4
2
+ Name: ims-mcp
3
+ Version: 1.0.0
4
+ Summary: Model Context Protocol server for IMS (Instruction Management Systems)
5
+ Author: Igor Solomatov
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/ims-mcp/
8
+ Keywords: mcp,ims,retrieval,rag,ai,llm,model-context-protocol,knowledge-base
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: r2r>=3.6.0
21
+ Requires-Dist: mcp>=1.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.0.0; extra == "dev"
24
+ Requires-Dist: twine>=4.0.0; extra == "dev"
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # ims-mcp
29
+
30
+ **Model Context Protocol (MCP) server for Rosetta (Enterprise Engineering Governance and Instructions Management System)**
31
+
32
+ *Powered by R2R technology for advanced RAG capabilities*
33
+
34
+ This package provides a FastMCP server that connects to IMS servers for advanced retrieval-augmented generation (RAG) capabilities. It enables AI assistants like Claude Desktop, Cursor, and other MCP clients to search, retrieve, and manage documents in Rosetta knowledge bases.
35
+
36
+ ## Features
37
+
38
+ - 🔍 **Semantic Search** - Vector-based and full-text search across documents
39
+ - 🤖 **RAG Queries** - Retrieval-augmented generation with configurable LLM settings
40
+ - 📝 **Document Management** - Upload, update, list, and delete documents with upsert semantics
41
+ - 🏷️ **Metadata Filtering** - Advanced filtering by tags, domain, and custom metadata
42
+ - 🌐 **Environment-Based Config** - Zero configuration, reads from environment variables
43
+
44
+ ## Installation
45
+
46
+ ### Using uvx (recommended)
47
+
48
+ The easiest way to use ims-mcp is with `uvx`, which automatically handles installation:
49
+
50
+ ```bash
51
+ uvx ims-mcp
52
+ ```
53
+
54
+ ### Using pip
55
+
56
+ Install globally or in a virtual environment:
57
+
58
+ ```bash
59
+ pip install ims-mcp
60
+ ```
61
+
62
+ Then run:
63
+
64
+ ```bash
65
+ ims-mcp
66
+ ```
67
+
68
+ ### As a Python Module
69
+
70
+ You can also run it as a module:
71
+
72
+ ```bash
73
+ python -m ims_mcp
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ The server automatically reads configuration from environment variables:
79
+
80
+ | Variable | Description | Default |
81
+ |----------|-------------|---------|
82
+ | `R2R_API_BASE` or `R2R_BASE_URL` | IMS server URL | `http://localhost:7272` |
83
+ | `R2R_COLLECTION` | Collection name for queries | Server default |
84
+ | `R2R_API_KEY` | API key for authentication | None |
85
+
86
+ **Note:** Environment variables use `R2R_` prefix for compatibility with the underlying R2R SDK.
87
+
88
+ ## Usage with MCP Clients
89
+
90
+ ### Cursor IDE
91
+
92
+ Add to `.cursor/mcp.json`:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "KnowledgeBase": {
98
+ "command": "uvx",
99
+ "args": ["ims-mcp"],
100
+ "env": {
101
+ "R2R_API_BASE": "http://localhost:7272",
102
+ "R2R_COLLECTION": "aia-r1"
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### Claude Desktop
110
+
111
+ Add to Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
112
+
113
+ ```json
114
+ {
115
+ "mcpServers": {
116
+ "ims": {
117
+ "command": "uvx",
118
+ "args": ["ims-mcp"],
119
+ "env": {
120
+ "R2R_API_BASE": "http://localhost:7272",
121
+ "R2R_COLLECTION": "my-collection"
122
+ }
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ ### Other MCP Clients
129
+
130
+ Any MCP client can use ims-mcp by specifying the command and environment variables:
131
+
132
+ ```json
133
+ {
134
+ "command": "uvx",
135
+ "args": ["ims-mcp"],
136
+ "env": {
137
+ "R2R_API_BASE": "http://localhost:7272"
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## Available MCP Tools
143
+
144
+ ### 1. search
145
+
146
+ Perform semantic and full-text search across documents.
147
+
148
+ **Parameters:**
149
+ - `query` (str): Search query
150
+ - `filters` (dict, optional): Metadata filters (e.g., `{"tags": {"$in": ["agents"]}}`)
151
+ - `limit` (int, optional): Maximum results
152
+ - `use_semantic_search` (bool, optional): Enable vector search
153
+ - `use_fulltext_search` (bool, optional): Enable full-text search
154
+
155
+ **Example:**
156
+ ```python
157
+ search("machine learning", filters={"tags": {"$in": ["research"]}}, limit=5)
158
+ ```
159
+
160
+ ### 2. rag
161
+
162
+ Retrieval-augmented generation with LLM.
163
+
164
+ **Parameters:**
165
+ - `query` (str): Question to answer
166
+ - `filters` (dict, optional): Metadata filters
167
+ - `limit` (int, optional): Max search results to use
168
+ - `model` (str, optional): LLM model name
169
+ - `temperature` (float, optional): Response randomness (0-1)
170
+ - `max_tokens` (int, optional): Max response length
171
+
172
+ **Example:**
173
+ ```python
174
+ rag("What is machine learning?", model="gpt-4", temperature=0.7)
175
+ ```
176
+
177
+ ### 3. put_document
178
+
179
+ Upload or update a document with upsert semantics.
180
+
181
+ **Parameters:**
182
+ - `content` (str): Document text content
183
+ - `title` (str): Document title
184
+ - `metadata` (dict, optional): Custom metadata (e.g., `{"tags": ["research"], "author": "John"}`)
185
+ - `document_id` (str, optional): Explicit document ID
186
+
187
+ **Example:**
188
+ ```python
189
+ put_document(
190
+ content="Machine learning is...",
191
+ title="ML Guide",
192
+ metadata={"tags": ["research", "ml"]}
193
+ )
194
+ ```
195
+
196
+ ### 4. list_documents
197
+
198
+ List documents with pagination and optional tag filtering.
199
+
200
+ **Parameters:**
201
+ - `offset` (int, optional): Documents to skip (default: 0)
202
+ - `limit` (int, optional): Max documents (default: 100)
203
+ - `document_ids` (list[str], optional): Specific IDs to retrieve
204
+ - `compact_view` (bool, optional): Show only ID and title (default: False)
205
+ - `tags` (list[str], optional): Filter by tags (e.g., `["agents", "r1"]`)
206
+ - `match_all_tags` (bool, optional): If True, document must have ALL tags; if False (default), document must have ANY tag
207
+
208
+ **Examples:**
209
+ ```python
210
+ # List all documents
211
+ list_documents(offset=0, limit=10, compact_view=False)
212
+
213
+ # Filter by tags (ANY mode - documents with "research" OR "ml")
214
+ list_documents(tags=["research", "ml"])
215
+
216
+ # Filter by tags (ALL mode - documents with both "research" AND "ml")
217
+ list_documents(tags=["research", "ml"], match_all_tags=True)
218
+ ```
219
+
220
+ **Note:** Tag filtering is performed client-side after fetching results. For large collections with complex filtering needs, consider using the `search()` tool with metadata filters instead.
221
+
222
+ ### 5. get_document
223
+
224
+ Retrieve a specific document by ID or title.
225
+
226
+ **Parameters:**
227
+ - `document_id` (str, optional): Document ID
228
+ - `title` (str, optional): Document title
229
+
230
+ **Example:**
231
+ ```python
232
+ get_document(title="ML Guide")
233
+ ```
234
+
235
+ ### 6. delete_document
236
+
237
+ Delete a document by ID.
238
+
239
+ **Parameters:**
240
+ - `document_id` (str, required): The unique identifier of the document to delete
241
+
242
+ **Example:**
243
+ ```python
244
+ delete_document(document_id="550e8400-e29b-41d4-a716-446655440000")
245
+ ```
246
+
247
+ **Returns:**
248
+ - Success message with document ID on successful deletion
249
+ - Error message if document not found or permission denied
250
+
251
+ ## Metadata Filtering
252
+
253
+ All filter operators supported:
254
+
255
+ - `$eq`: Equal
256
+ - `$neq`: Not equal
257
+ - `$gt`, `$gte`: Greater than (or equal)
258
+ - `$lt`, `$lte`: Less than (or equal)
259
+ - `$in`: In array
260
+ - `$nin`: Not in array
261
+ - `$like`, `$ilike`: Pattern matching (case-sensitive/insensitive)
262
+
263
+ **Examples:**
264
+
265
+ ```python
266
+ # Filter by tags
267
+ filters={"tags": {"$in": ["research", "ml"]}}
268
+
269
+ # Filter by domain
270
+ filters={"domain": {"$eq": "instructions"}}
271
+
272
+ # Combined filters
273
+ filters={"tags": {"$in": ["research"]}, "created_at": {"$gte": "2024-01-01"}}
274
+ ```
275
+
276
+ ## Development
277
+
278
+ ### Local Installation
279
+
280
+ Install directly from PyPI:
281
+
282
+ ```bash
283
+ pip install ims-mcp
284
+ ```
285
+
286
+ Or for the latest development version, install from source if you have the code locally:
287
+
288
+ ```bash
289
+ pip install -e .
290
+ ```
291
+
292
+ ### Running Tests
293
+
294
+ ```bash
295
+ pip install -e ".[dev]"
296
+ pytest
297
+ ```
298
+
299
+ ### Building for Distribution
300
+
301
+ ```bash
302
+ python -m build
303
+ ```
304
+
305
+ ## Requirements
306
+
307
+ - Python >= 3.10
308
+ - IMS server running and accessible (powered by R2R Light)
309
+ - r2r Python SDK >= 3.6.0
310
+ - mcp >= 1.0.0
311
+
312
+ ## License
313
+
314
+ MIT License - see LICENSE file for details
315
+
316
+ This package is built on R2R (RAG to Riches) technology by SciPhi AI, which is licensed under the MIT License. We gratefully acknowledge the R2R project and its contributors.
317
+
318
+ ## Links
319
+
320
+ - **R2R Technology**: https://github.com/SciPhi-AI/R2R
321
+ - **Model Context Protocol**: https://modelcontextprotocol.io/
322
+ - **FastMCP**: https://github.com/jlowin/fastmcp
323
+
324
+ ## Support
325
+
326
+ For issues and questions, visit the package page: https://pypi.org/project/ims-mcp/
@@ -0,0 +1,9 @@
1
+ ims_mcp/__init__.py,sha256=qhmOG7M_udp7EkUXUT-7cXTmD3RoWoC9r9NOv2Kjzn8,631
2
+ ims_mcp/__main__.py,sha256=z4P1aCVfOgS3cTM2wgJd2pxjMmKCkGkiqYDRGgrspxw,191
3
+ ims_mcp/server.py,sha256=Gk3-R8g6E3ULxuMCCfNmqpAa6_cw4boiy5WTWO5smqc,19384
4
+ ims_mcp-1.0.0.dist-info/licenses/LICENSE,sha256=4d1dlH04mbnN3ya4lybcVOUwljRHGy-aSc9MYqGYW44,2534
5
+ ims_mcp-1.0.0.dist-info/METADATA,sha256=X8PxT1G_8is-WbkQOOKIzOfecnKQ65mNxtkiJMbodTY,8350
6
+ ims_mcp-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ ims_mcp-1.0.0.dist-info/entry_points.txt,sha256=xCH9I8g1pTTEqrfjnE-ANHaZo4W6EBJVy0Lg5z8SaIQ,48
8
+ ims_mcp-1.0.0.dist-info/top_level.txt,sha256=wEXA33qFr_eov3S1PY2OF6EQBA2rtAWB_ZNJOzNNQuM,8
9
+ ims_mcp-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ims-mcp = ims_mcp.server:main
@@ -0,0 +1,52 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Igor Solomatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ================================================================================
24
+ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
25
+ ================================================================================
26
+
27
+ This package integrates with and depends on R2R (RAG to Riches), which is
28
+ licensed under the MIT License:
29
+
30
+ R2R - Advanced AI Retrieval System
31
+ Copyright (c) 2024 SciPhi AI
32
+ Repository: https://github.com/SciPhi-AI/R2R
33
+ License: MIT
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining a copy
36
+ of this software and associated documentation files (the "Software"), to deal
37
+ in the Software without restriction, including without limitation the rights
38
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
+ copies of the Software, and to permit persons to whom the Software is
40
+ furnished to do so, subject to the following conditions:
41
+
42
+ The above copyright notice and this permission notice shall be included in all
43
+ copies or substantial portions of the Software.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
51
+ SOFTWARE.
52
+
@@ -0,0 +1 @@
1
+ ims_mcp