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 +20 -0
- ims_mcp/__main__.py +11 -0
- ims_mcp/server.py +512 -0
- ims_mcp-1.0.0.dist-info/METADATA +326 -0
- ims_mcp-1.0.0.dist-info/RECORD +9 -0
- ims_mcp-1.0.0.dist-info/WHEEL +5 -0
- ims_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- ims_mcp-1.0.0.dist-info/licenses/LICENSE +52 -0
- ims_mcp-1.0.0.dist-info/top_level.txt +1 -0
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
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,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
|